Repository: testcontainers/testcontainers-java Branch: main Commit: 18e55b8dd8c5 Files: 1372 Total size: 3.2 MB Directory structure: gitextract_vplit3v1/ ├── .circleci/ │ └── config.yml ├── .devcontainer/ │ └── devcontainer.json ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yaml │ │ ├── config.yml │ │ ├── enhancement.yaml │ │ └── feature.yaml │ ├── actions/ │ │ ├── setup-build/ │ │ │ └── action.yml │ │ ├── setup-gradle/ │ │ │ └── action.yml │ │ ├── setup-java/ │ │ │ └── action.yml │ │ └── setup-junit-report/ │ │ └── action.yml │ ├── bumper.yml │ ├── dependabot.yml │ ├── labeler.yml │ ├── pull_request_template.md │ ├── release-drafter.yml │ ├── settings.yml │ └── workflows/ │ ├── ci-docker-wormhole.yml │ ├── ci-rootless.yml │ ├── ci-windows-trigger.yml │ ├── ci-windows.yml │ ├── ci.yml │ ├── combine-prs.yml │ ├── labeler.yml │ ├── moby-latest.yml │ ├── release-drafter.yml │ ├── release.yml │ ├── scripts/ │ │ └── check_ci_status.sh │ ├── update-docs-version.yml │ ├── update-gradle-wrapper.yml │ └── update-testcontainers-version.yml ├── .gitignore ├── .sdkmanrc ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RELEASING.md ├── annotations/ │ ├── com/ │ │ └── google/ │ │ └── common/ │ │ └── base/ │ │ └── annotations.xml │ └── org/ │ └── rnorth/ │ └── ducttape/ │ └── annotations.xml ├── azure-pipelines.yml ├── bom/ │ └── build.gradle ├── build.gradle ├── buildSrc/ │ ├── build.gradle │ └── src/ │ └── main/ │ └── groovy/ │ └── org/ │ └── testcontainers/ │ └── build/ │ ├── ComparePOMWithLatestReleasedTask.groovy │ └── DelombokArgumentProvider.groovy ├── config/ │ └── checkstyle/ │ └── checkstyle.xml ├── core/ │ ├── build.gradle │ ├── src/ │ │ ├── jarFileTest/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── AbstractJarFileTest.java │ │ │ ├── JarFileShadingTest.java │ │ │ └── PublicBinaryAPITest.java │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ ├── DelegatingDockerClient.java │ │ │ │ ├── DockerClientFactory.java │ │ │ │ ├── Testcontainers.java │ │ │ │ ├── UnstableAPI.java │ │ │ │ ├── containers/ │ │ │ │ │ ├── BindMode.java │ │ │ │ │ ├── ComposeCommand.java │ │ │ │ │ ├── ComposeContainer.java │ │ │ │ │ ├── ComposeDelegate.java │ │ │ │ │ ├── ComposeServiceWaitStrategyTarget.java │ │ │ │ │ ├── Container.java │ │ │ │ │ ├── ContainerDef.java │ │ │ │ │ ├── ContainerFetchException.java │ │ │ │ │ ├── ContainerLaunchException.java │ │ │ │ │ ├── ContainerState.java │ │ │ │ │ ├── ContainerisedDockerCompose.java │ │ │ │ │ ├── DockerCompose.java │ │ │ │ │ ├── DockerComposeContainer.java │ │ │ │ │ ├── DockerComposeFiles.java │ │ │ │ │ ├── DockerMcpGatewayContainer.java │ │ │ │ │ ├── DockerModelRunnerContainer.java │ │ │ │ │ ├── ExecConfig.java │ │ │ │ │ ├── ExecInContainerPattern.java │ │ │ │ │ ├── FixedHostPortGenericContainer.java │ │ │ │ │ ├── FutureContainer.java │ │ │ │ │ ├── GenericContainer.java │ │ │ │ │ ├── InternetProtocol.java │ │ │ │ │ ├── LocalDockerCompose.java │ │ │ │ │ ├── Network.java │ │ │ │ │ ├── ParsedDockerComposeFile.java │ │ │ │ │ ├── PortForwardingContainer.java │ │ │ │ │ ├── SelinuxContext.java │ │ │ │ │ ├── SocatContainer.java │ │ │ │ │ ├── VncRecordingContainer.java │ │ │ │ │ ├── output/ │ │ │ │ │ │ ├── BaseConsumer.java │ │ │ │ │ │ ├── FrameConsumerResultCallback.java │ │ │ │ │ │ ├── OutputFrame.java │ │ │ │ │ │ ├── Slf4jLogConsumer.java │ │ │ │ │ │ ├── ToStringConsumer.java │ │ │ │ │ │ └── WaitingConsumer.java │ │ │ │ │ ├── startupcheck/ │ │ │ │ │ │ ├── IndefiniteWaitOneShotStartupCheckStrategy.java │ │ │ │ │ │ ├── IsRunningStartupCheckStrategy.java │ │ │ │ │ │ ├── MinimumDurationRunningStartupCheckStrategy.java │ │ │ │ │ │ ├── OneShotStartupCheckStrategy.java │ │ │ │ │ │ └── StartupCheckStrategy.java │ │ │ │ │ ├── traits/ │ │ │ │ │ │ └── LinkableContainer.java │ │ │ │ │ └── wait/ │ │ │ │ │ ├── internal/ │ │ │ │ │ │ ├── ExternalPortListeningCheck.java │ │ │ │ │ │ └── InternalCommandPortListeningCheck.java │ │ │ │ │ └── strategy/ │ │ │ │ │ ├── AbstractWaitStrategy.java │ │ │ │ │ ├── DockerHealthcheckWaitStrategy.java │ │ │ │ │ ├── HostPortWaitStrategy.java │ │ │ │ │ ├── HttpWaitStrategy.java │ │ │ │ │ ├── LogMessageWaitStrategy.java │ │ │ │ │ ├── ShellStrategy.java │ │ │ │ │ ├── Wait.java │ │ │ │ │ ├── WaitAllStrategy.java │ │ │ │ │ ├── WaitStrategy.java │ │ │ │ │ └── WaitStrategyTarget.java │ │ │ │ ├── core/ │ │ │ │ │ └── CreateContainerCmdModifier.java │ │ │ │ ├── dockerclient/ │ │ │ │ │ ├── AuditLoggingDockerClient.java │ │ │ │ │ ├── AuthDelegatingDockerClientConfig.java │ │ │ │ │ ├── DockerClientConfigUtils.java │ │ │ │ │ ├── DockerClientProviderStrategy.java │ │ │ │ │ ├── DockerDesktopClientProviderStrategy.java │ │ │ │ │ ├── DockerMachineClientProviderStrategy.java │ │ │ │ │ ├── EnvironmentAndSystemPropertyClientProviderStrategy.java │ │ │ │ │ ├── HeadersAddingDockerHttpClient.java │ │ │ │ │ ├── InvalidConfigurationException.java │ │ │ │ │ ├── LogToStringContainerCallback.java │ │ │ │ │ ├── NpipeSocketClientProviderStrategy.java │ │ │ │ │ ├── RootlessDockerClientProviderStrategy.java │ │ │ │ │ ├── TestcontainersHostPropertyClientProviderStrategy.java │ │ │ │ │ ├── TransportConfig.java │ │ │ │ │ └── UnixSocketClientProviderStrategy.java │ │ │ │ ├── images/ │ │ │ │ │ ├── AbstractImagePullPolicy.java │ │ │ │ │ ├── AgeBasedPullPolicy.java │ │ │ │ │ ├── AlwaysPullPolicy.java │ │ │ │ │ ├── DefaultPullPolicy.java │ │ │ │ │ ├── ImageData.java │ │ │ │ │ ├── ImagePullPolicy.java │ │ │ │ │ ├── LocalImagesCache.java │ │ │ │ │ ├── LoggedPullImageResultCallback.java │ │ │ │ │ ├── ParsedDockerfile.java │ │ │ │ │ ├── PullPolicy.java │ │ │ │ │ ├── RemoteDockerImage.java │ │ │ │ │ ├── TimeLimitedLoggedPullImageResultCallback.java │ │ │ │ │ └── builder/ │ │ │ │ │ ├── ImageFromDockerfile.java │ │ │ │ │ ├── Transferable.java │ │ │ │ │ ├── dockerfile/ │ │ │ │ │ │ ├── DockerfileBuilder.java │ │ │ │ │ │ ├── statement/ │ │ │ │ │ │ │ ├── KeyValuesStatement.java │ │ │ │ │ │ │ ├── MultiArgsStatement.java │ │ │ │ │ │ │ ├── RawStatement.java │ │ │ │ │ │ │ ├── SingleArgumentStatement.java │ │ │ │ │ │ │ └── Statement.java │ │ │ │ │ │ └── traits/ │ │ │ │ │ │ ├── AddStatementTrait.java │ │ │ │ │ │ ├── CmdStatementTrait.java │ │ │ │ │ │ ├── CopyStatementTrait.java │ │ │ │ │ │ ├── DockerfileBuilderTrait.java │ │ │ │ │ │ ├── EntryPointStatementTrait.java │ │ │ │ │ │ ├── EnvStatementTrait.java │ │ │ │ │ │ ├── ExposeStatementTrait.java │ │ │ │ │ │ ├── FromStatementTrait.java │ │ │ │ │ │ ├── LabelStatementTrait.java │ │ │ │ │ │ ├── RunStatementTrait.java │ │ │ │ │ │ ├── UserStatementTrait.java │ │ │ │ │ │ ├── VolumeStatementTrait.java │ │ │ │ │ │ └── WorkdirStatementTrait.java │ │ │ │ │ └── traits/ │ │ │ │ │ ├── BuildContextBuilderTrait.java │ │ │ │ │ ├── ClasspathTrait.java │ │ │ │ │ ├── DockerfileTrait.java │ │ │ │ │ ├── FilesTrait.java │ │ │ │ │ └── StringsTrait.java │ │ │ │ ├── jib/ │ │ │ │ │ ├── JibDockerClient.java │ │ │ │ │ ├── JibImage.java │ │ │ │ │ └── JibImageDetails.java │ │ │ │ ├── lifecycle/ │ │ │ │ │ ├── Startable.java │ │ │ │ │ ├── Startables.java │ │ │ │ │ ├── TestDescription.java │ │ │ │ │ └── TestLifecycleAware.java │ │ │ │ └── utility/ │ │ │ │ ├── AuditLogger.java │ │ │ │ ├── AuthConfigUtil.java │ │ │ │ ├── Base58.java │ │ │ │ ├── ClasspathScanner.java │ │ │ │ ├── CommandLine.java │ │ │ │ ├── ComparableVersion.java │ │ │ │ ├── ConfigurationFileImageNameSubstitutor.java │ │ │ │ ├── DefaultImageNameSubstitutor.java │ │ │ │ ├── DockerImageName.java │ │ │ │ ├── DockerLoggerFactory.java │ │ │ │ ├── DockerMachineClient.java │ │ │ │ ├── DockerStatus.java │ │ │ │ ├── DynamicPollInterval.java │ │ │ │ ├── ImageNameSubstitutor.java │ │ │ │ ├── JVMHookResourceReaper.java │ │ │ │ ├── LazyFuture.java │ │ │ │ ├── LicenseAcceptance.java │ │ │ │ ├── LogUtils.java │ │ │ │ ├── MountableFile.java │ │ │ │ ├── PathUtils.java │ │ │ │ ├── PrefixingImageNameSubstitutor.java │ │ │ │ ├── RegistryAuthLocator.java │ │ │ │ ├── ResourceReaper.java │ │ │ │ ├── RyukContainer.java │ │ │ │ ├── RyukResourceReaper.java │ │ │ │ ├── TestEnvironment.java │ │ │ │ ├── TestcontainersConfiguration.java │ │ │ │ ├── ThrowingFunction.java │ │ │ │ └── Versioning.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ ├── native-image/ │ │ │ │ └── org.testcontainers/ │ │ │ │ └── testcontainers/ │ │ │ │ └── native-image.properties │ │ │ └── services/ │ │ │ └── org.testcontainers.dockerclient.DockerClientProviderStrategy │ │ └── test/ │ │ ├── java/ │ │ │ ├── alt/ │ │ │ │ └── testcontainers/ │ │ │ │ ├── README.md │ │ │ │ └── images/ │ │ │ │ └── OutOfPackageImagePullPolicyTest.java │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── DaemonTest.java │ │ │ ├── DockerClientFactoryTest.java │ │ │ ├── DockerRegistryContainer.java │ │ │ ├── TestImages.java │ │ │ ├── containers/ │ │ │ │ ├── ComposeContainerTest.java │ │ │ │ ├── ComposeContainerWithServicesTest.java │ │ │ │ ├── ComposeOverridesTest.java │ │ │ │ ├── ComposeProfilesOptionTest.java │ │ │ │ ├── ContainerStateTest.java │ │ │ │ ├── DockerComposeContainerCustomImageTest.java │ │ │ │ ├── DockerComposeContainerWithServicesTest.java │ │ │ │ ├── DockerComposeFilesTest.java │ │ │ │ ├── DockerComposeOverridesTest.java │ │ │ │ ├── DockerComposeProfilesOptionTest.java │ │ │ │ ├── DockerMcpGatewayContainerTest.java │ │ │ │ ├── DockerModelRunnerContainerTest.java │ │ │ │ ├── ExposedHostTest.java │ │ │ │ ├── GenericContainerTest.java │ │ │ │ ├── JibTest.java │ │ │ │ ├── MultiStageBuildTest.java │ │ │ │ ├── NetworkTest.java │ │ │ │ ├── ParsedDockerComposeFileBean.java │ │ │ │ ├── ParsedDockerComposeFileValidationTest.java │ │ │ │ ├── ReusabilityUnitTests.java │ │ │ │ ├── output/ │ │ │ │ │ ├── ContainerLogsTest.java │ │ │ │ │ ├── FrameConsumerResultCallbackTest.java │ │ │ │ │ └── ToStringConsumerTest.java │ │ │ │ ├── startupcheck/ │ │ │ │ │ └── IsRunningStartupCheckStrategyTest.java │ │ │ │ └── wait/ │ │ │ │ ├── internal/ │ │ │ │ │ ├── ExternalPortListeningCheckTest.java │ │ │ │ │ └── InternalCommandPortListeningCheckTest.java │ │ │ │ └── strategy/ │ │ │ │ ├── DockerHealthcheckWaitStrategyTest.java │ │ │ │ └── WaitAllStrategyTest.java │ │ │ ├── custom/ │ │ │ │ └── TestCreateContainerCmdModifier.java │ │ │ ├── dockerclient/ │ │ │ │ ├── AmbiguousImagePullTest.java │ │ │ │ ├── DockerClientConfigUtilsTest.java │ │ │ │ ├── EnvironmentAndSystemPropertyClientProviderStrategyTest.java │ │ │ │ ├── EventStreamTest.java │ │ │ │ ├── ImagePullTest.java │ │ │ │ └── TestcontainersHostPropertyClientProviderStrategyTest.java │ │ │ ├── images/ │ │ │ │ ├── AgeBasedPullPolicyTest.java │ │ │ │ ├── ImageDataTest.java │ │ │ │ ├── ImagePullPolicyTest.java │ │ │ │ ├── LocalImagesCacheAccessor.java │ │ │ │ ├── OverrideImagePullPolicyTest.java │ │ │ │ ├── ParsedDockerfileTest.java │ │ │ │ ├── RemoteDockerImageTest.java │ │ │ │ └── builder/ │ │ │ │ ├── DockerfileBuildTest.java │ │ │ │ ├── DockerignoreTest.java │ │ │ │ ├── ImageFromDockerfileTest.java │ │ │ │ └── dockerfile/ │ │ │ │ └── statement/ │ │ │ │ ├── AbstractStatementTest.java │ │ │ │ ├── KeyValuesStatementTest.java │ │ │ │ ├── MultiArgsStatementTest.java │ │ │ │ ├── RawStatementTest.java │ │ │ │ └── SingleArgumentStatementTest.java │ │ │ ├── junit/ │ │ │ │ ├── BaseComposeTest.java │ │ │ │ ├── BaseDockerComposeTest.java │ │ │ │ ├── ComposeContainerOverrideTest.java │ │ │ │ ├── ComposeContainerPortViaEnvTest.java │ │ │ │ ├── ComposeContainerScalingTest.java │ │ │ │ ├── ComposeContainerTest.java │ │ │ │ ├── ComposeContainerVolumeRemovalTest.java │ │ │ │ ├── ComposeContainerWithBuildTest.java │ │ │ │ ├── ComposeContainerWithCopyFilesTest.java │ │ │ │ ├── ComposeContainerWithOptionsTest.java │ │ │ │ ├── ComposeContainerWithWaitStrategiesTest.java │ │ │ │ ├── ComposeErrorHandlingTest.java │ │ │ │ ├── ComposePassthroughTest.java │ │ │ │ ├── ComposeWaitStrategyTest.java │ │ │ │ ├── ComposeWithIdentifierTest.java │ │ │ │ ├── ComposeWithNetworkTest.java │ │ │ │ ├── CopyFileToContainerTest.java │ │ │ │ ├── DependenciesTest.java │ │ │ │ ├── DockerComposeContainerPortViaEnvTest.java │ │ │ │ ├── DockerComposeContainerScalingTest.java │ │ │ │ ├── DockerComposeContainerTest.java │ │ │ │ ├── DockerComposeContainerVolumeRemovalTest.java │ │ │ │ ├── DockerComposeContainerWithBuildTest.java │ │ │ │ ├── DockerComposeContainerWithCopyFilesTest.java │ │ │ │ ├── DockerComposeContainerWithOptionsTest.java │ │ │ │ ├── DockerComposeErrorHandlingTest.java │ │ │ │ ├── DockerComposeLocalImageTest.java │ │ │ │ ├── DockerComposeLogConsumerTest.java │ │ │ │ ├── DockerComposePassthroughTest.java │ │ │ │ ├── DockerComposeServiceTest.java │ │ │ │ ├── DockerComposeV2FormatTest.java │ │ │ │ ├── DockerComposeV2FormatWithIdentifierTest.java │ │ │ │ ├── DockerComposeV2WithNetworkTest.java │ │ │ │ ├── DockerComposeWaitStrategyTest.java │ │ │ │ ├── DockerNetworkModeTest.java │ │ │ │ ├── DockerfileContainerTest.java │ │ │ │ ├── DockerfileTest.java │ │ │ │ ├── ExecInContainerTest.java │ │ │ │ ├── FileOperationsTest.java │ │ │ │ ├── FixedHostPortContainerTest.java │ │ │ │ ├── GenericContainerRuleTest.java │ │ │ │ ├── NonExistentImagePullTest.java │ │ │ │ ├── OutputStreamTest.java │ │ │ │ ├── OutputStreamWithTTYTest.java │ │ │ │ ├── ParameterizedDockerfileContainerTest.java │ │ │ │ ├── WorkingDirectoryTest.java │ │ │ │ └── wait/ │ │ │ │ └── strategy/ │ │ │ │ ├── AbstractWaitStrategyTest.java │ │ │ │ ├── HostPortWaitStrategyTest.java │ │ │ │ ├── HttpWaitStrategyTest.java │ │ │ │ ├── LogMessageWaitStrategyTest.java │ │ │ │ └── ShellStrategyTest.java │ │ │ └── utility/ │ │ │ ├── AuthenticatedImagePullTest.java │ │ │ ├── ClasspathScannerTest.java │ │ │ ├── ComparableVersionTest.java │ │ │ ├── DefaultImageNameSubstitutorTest.java │ │ │ ├── DirectoryTarResourceTest.java │ │ │ ├── DockerImageNameCompatibilityTest.java │ │ │ ├── DockerImageNameTest.java │ │ │ ├── DockerLoggerFactoryTest.java │ │ │ ├── DockerStatusTest.java │ │ │ ├── FakeImagePullPolicy.java │ │ │ ├── FakeImageSubstitutor.java │ │ │ ├── FilterRegistryTest.java │ │ │ ├── ImageNameSubstitutorTest.java │ │ │ ├── LazyFutureTest.java │ │ │ ├── LicenseAcceptanceTest.java │ │ │ ├── MockTestcontainersConfigurationExtension.java │ │ │ ├── MountableFileTest.java │ │ │ ├── PrefixingImageNameSubstitutorTest.java │ │ │ ├── RegistryAuthLocatorTest.java │ │ │ ├── ResourceReaperTest.java │ │ │ ├── TestEnvironmentTest.java │ │ │ └── TestcontainersConfigurationTest.java │ │ └── resources/ │ │ ├── Dockerfile │ │ ├── Dockerfile-multistage │ │ ├── META-INF/ │ │ │ └── services/ │ │ │ └── org.testcontainers.core.CreateContainerCmdModifier │ │ ├── auth-config/ │ │ │ ├── config-basic-auth.json │ │ │ ├── config-empty-auth-with-helper.json │ │ │ ├── config-empty.json │ │ │ ├── config-existing-auth-with-helper.json │ │ │ ├── config-with-helper-and-store.json │ │ │ ├── config-with-helper-no-server-url-using-token.json │ │ │ ├── config-with-helper-no-server-url.json │ │ │ ├── config-with-helper-using-token.json │ │ │ ├── config-with-helper.json │ │ │ ├── config-with-json-key.json │ │ │ ├── config-with-store-empty.json │ │ │ ├── config-with-store.json │ │ │ ├── docker-credential-fake │ │ │ └── win/ │ │ │ └── docker-credential-fake.bat │ │ ├── compose-build-test/ │ │ │ ├── Dockerfile │ │ │ └── docker-compose.yml │ │ ├── compose-dockerfile/ │ │ │ ├── Dockerfile │ │ │ └── passthrough.sh │ │ ├── compose-file-copy-inclusions/ │ │ │ ├── Dockerfile │ │ │ ├── EnvVariableRestEndpoint.java │ │ │ ├── compose-root-only.yml │ │ │ ├── compose-test-only.yml │ │ │ └── compose.yml │ │ ├── compose-options-test/ │ │ │ └── with-deploy-block.yml │ │ ├── compose-override/ │ │ │ ├── compose-override.yml │ │ │ └── compose.yml │ │ ├── compose-profile-option/ │ │ │ └── compose-test.yml │ │ ├── compose-scaling-multiple-containers.yml │ │ ├── compose-test.yml │ │ ├── compose-v2-build-test/ │ │ │ ├── Dockerfile │ │ │ └── docker-compose.yml │ │ ├── compose-with-inline-scale-test.yml │ │ ├── composev2/ │ │ │ ├── compose-test.yml │ │ │ └── scaled-compose-test.yml │ │ ├── container-license-acceptance.txt │ │ ├── docker-compose-base.yml │ │ ├── docker-compose-container-name-v1.yml │ │ ├── docker-compose-deserialization.yml │ │ ├── docker-compose-healthcheck.yml │ │ ├── docker-compose-imagename-overriding-a.yml │ │ ├── docker-compose-imagename-overriding-b.yml │ │ ├── docker-compose-imagename-parsing-dockerfile-with-context.yml │ │ ├── docker-compose-imagename-parsing-dockerfile.yml │ │ ├── docker-compose-imagename-parsing-v1.yml │ │ ├── docker-compose-imagename-parsing-v2-no-version.yml │ │ ├── docker-compose-imagename-parsing-v2.yml │ │ ├── docker-compose-non-default-override.yml │ │ ├── dockerfile-build-invalid/ │ │ │ ├── .dockerignore │ │ │ └── Dockerfile │ │ ├── dockerfile-build-test/ │ │ │ ├── .dockerignore │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile-alt │ │ │ ├── Dockerfile-buildarg │ │ │ ├── Dockerfile-currentdir │ │ │ ├── Dockerfile-from-buildarg │ │ │ ├── localfile.txt │ │ │ ├── should_be_ignored.txt │ │ │ └── should_not_be_ignored.txt │ │ ├── expectedClasspathFile.txt │ │ ├── fixtures/ │ │ │ └── statements/ │ │ │ ├── KeyValuesStatementTest/ │ │ │ │ ├── keyWithNewLinesTest │ │ │ │ ├── keyWithSpacesTest │ │ │ │ ├── keyWithTabsTest │ │ │ │ ├── multilineTest │ │ │ │ └── valueIsEscapedTest │ │ │ ├── MultiArgsStatementTest/ │ │ │ │ ├── multilineTest │ │ │ │ └── simpleTest │ │ │ ├── RawStatementTest/ │ │ │ │ └── simpleTest │ │ │ └── SingleArgumentStatementTest/ │ │ │ ├── multilineTest │ │ │ └── simpleTest │ │ ├── health-wait-strategy-dockerfile/ │ │ │ ├── Dockerfile │ │ │ └── write_file_and_loop.sh │ │ ├── https-wait-strategy-dockerfile/ │ │ │ ├── Dockerfile │ │ │ └── nginx-ssl.conf │ │ ├── internal-port-check-dockerfile/ │ │ │ ├── Dockerfile-bash │ │ │ ├── Dockerfile-nc │ │ │ ├── Dockerfile-tcp │ │ │ └── nginx.conf │ │ ├── invalid-compose.yml │ │ ├── local-compose-test.yml │ │ ├── logback-test.xml │ │ ├── mappable-dockerfile/ │ │ │ └── Dockerfile │ │ ├── mappable-resource/ │ │ │ └── test-resource.txt │ │ ├── redis.conf │ │ ├── scaled-compose-test.yml │ │ ├── test-recursive-file.txt │ │ ├── test_copy_to_container.txt │ │ ├── v2-compose-test-passthrough.yml │ │ ├── v2-compose-test-port-via-env.yml │ │ ├── v2-compose-test-with-network.yml │ │ └── v2-compose-test.yml │ └── testlib/ │ ├── META-INF/ │ │ └── dummy_unique_name.txt │ ├── README.md │ ├── create_fakejar.sh │ ├── recursive/ │ │ └── dir/ │ │ └── content.txt │ └── repo/ │ └── fakejar/ │ └── fakejar/ │ ├── 0/ │ │ ├── fakejar-0.jar │ │ └── fakejar-0.pom │ └── maven-metadata-local.xml ├── docker-compose.yml ├── docs/ │ ├── _headers │ ├── _redirects │ ├── bounty.md │ ├── contributing.md │ ├── contributing_docs.md │ ├── css/ │ │ ├── extra.css │ │ └── tc-header.css │ ├── error_missing_container_runtime_environment.md │ ├── examples/ │ │ ├── junit4/ │ │ │ ├── generic/ │ │ │ │ ├── build.gradle │ │ │ │ └── src/ │ │ │ │ └── test/ │ │ │ │ ├── java/ │ │ │ │ │ ├── generic/ │ │ │ │ │ │ ├── CmdModifierTest.java │ │ │ │ │ │ ├── CommandsTest.java │ │ │ │ │ │ ├── ContainerCreationTest.java │ │ │ │ │ │ ├── ContainerLabelTest.java │ │ │ │ │ │ ├── DependsOnTest.java │ │ │ │ │ │ ├── ExampleImageNameSubstitutor.java │ │ │ │ │ │ ├── ExecTest.java │ │ │ │ │ │ ├── HostPortExposedTest.java │ │ │ │ │ │ ├── ImageNameSubstitutionTest.java │ │ │ │ │ │ ├── MultiplePortsExposedTest.java │ │ │ │ │ │ ├── WaitStrategiesTest.java │ │ │ │ │ │ └── support/ │ │ │ │ │ │ └── TestSpecificImageNameSubstitutor.java │ │ │ │ │ └── org/ │ │ │ │ │ └── testcontainers/ │ │ │ │ │ └── containers/ │ │ │ │ │ └── startupcheck/ │ │ │ │ │ └── StartupCheckStrategyTest.java │ │ │ │ └── resources/ │ │ │ │ ├── logback-test.xml │ │ │ │ └── testcontainers.properties │ │ │ └── redis/ │ │ │ ├── build.gradle │ │ │ └── src/ │ │ │ ├── main/ │ │ │ │ └── java/ │ │ │ │ └── quickstart/ │ │ │ │ └── RedisBackedCache.java │ │ │ └── test/ │ │ │ ├── java/ │ │ │ │ └── quickstart/ │ │ │ │ ├── RedisBackedCacheIntTest.java │ │ │ │ └── RedisBackedCacheIntTestStep0.java │ │ │ └── resources/ │ │ │ └── logback-test.xml │ │ ├── junit5/ │ │ │ └── redis/ │ │ │ ├── build.gradle │ │ │ └── src/ │ │ │ ├── main/ │ │ │ │ └── java/ │ │ │ │ └── quickstart/ │ │ │ │ └── RedisBackedCache.java │ │ │ └── test/ │ │ │ ├── java/ │ │ │ │ └── quickstart/ │ │ │ │ ├── RedisBackedCacheIntTest.java │ │ │ │ └── RedisBackedCacheIntTestStep0.java │ │ │ └── resources/ │ │ │ └── logback-test.xml │ │ └── spock/ │ │ └── redis/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── quickstart/ │ │ │ └── RedisBackedCache.java │ │ └── test/ │ │ ├── groovy/ │ │ │ └── quickstart/ │ │ │ ├── RedisBackedCacheIntTest.groovy │ │ │ └── RedisBackedCacheIntTestStep0.groovy │ │ └── resources/ │ │ └── logback-test.xml │ ├── examples.md │ ├── features/ │ │ ├── advanced_options.md │ │ ├── commands.md │ │ ├── configuration.md │ │ ├── container_logs.md │ │ ├── creating_container.md │ │ ├── creating_images.md │ │ ├── files.md │ │ ├── image_name_substitution.md │ │ ├── jib.md │ │ ├── networking.md │ │ ├── reuse.md │ │ └── startup_and_waits.md │ ├── getting_help.md │ ├── index.md │ ├── jitpack_dependencies.md │ ├── js/ │ │ └── tc-header.js │ ├── modules/ │ │ ├── activemq.md │ │ ├── azure.md │ │ ├── chromadb.md │ │ ├── consul.md │ │ ├── databases/ │ │ │ ├── cassandra.md │ │ │ ├── clickhouse.md │ │ │ ├── cockroachdb.md │ │ │ ├── couchbase.md │ │ │ ├── cratedb.md │ │ │ ├── databend.md │ │ │ ├── db2.md │ │ │ ├── index.md │ │ │ ├── influxdb.md │ │ │ ├── jdbc.md │ │ │ ├── mariadb.md │ │ │ ├── mongodb.md │ │ │ ├── mssqlserver.md │ │ │ ├── mysql.md │ │ │ ├── neo4j.md │ │ │ ├── oceanbase.md │ │ │ ├── oraclefree.md │ │ │ ├── oraclexe.md │ │ │ ├── orientdb.md │ │ │ ├── postgres.md │ │ │ ├── presto.md │ │ │ ├── questdb.md │ │ │ ├── r2dbc.md │ │ │ ├── scylladb.md │ │ │ ├── tidb.md │ │ │ ├── timeplus.md │ │ │ ├── trino.md │ │ │ └── yugabytedb.md │ │ ├── docker_compose.md │ │ ├── docker_mcp_gateway.md │ │ ├── docker_model_runner.md │ │ ├── elasticsearch.md │ │ ├── gcloud.md │ │ ├── grafana.md │ │ ├── hivemq.md │ │ ├── k3s.md │ │ ├── k6.md │ │ ├── kafka.md │ │ ├── ldap.md │ │ ├── localstack.md │ │ ├── milvus.md │ │ ├── minio.md │ │ ├── mockserver.md │ │ ├── nginx.md │ │ ├── ollama.md │ │ ├── openfga.md │ │ ├── pinecone.md │ │ ├── pulsar.md │ │ ├── qdrant.md │ │ ├── rabbitmq.md │ │ ├── redpanda.md │ │ ├── solace.md │ │ ├── solr.md │ │ ├── toxiproxy.md │ │ ├── typesense.md │ │ ├── vault.md │ │ ├── weaviate.md │ │ └── webdriver_containers.md │ ├── quickstart/ │ │ ├── junit_4_quickstart.md │ │ ├── junit_5_quickstart.md │ │ └── spock_quickstart.md │ ├── supported_docker_environment/ │ │ ├── continuous_integration/ │ │ │ ├── aws_codebuild.md │ │ │ ├── bitbucket_pipelines.md │ │ │ ├── circle_ci.md │ │ │ ├── concourse_ci.md │ │ │ ├── dind_patterns.md │ │ │ ├── drone.md │ │ │ ├── gitlab_ci.md │ │ │ ├── tekton.md │ │ │ └── travis.md │ │ ├── image_registry_rate_limiting.md │ │ ├── index.md │ │ ├── logging_config.md │ │ └── windows.md │ ├── test_framework_integration/ │ │ ├── external.md │ │ ├── junit_4.md │ │ ├── junit_5.md │ │ ├── manual_lifecycle_control.md │ │ └── spock.md │ └── theme/ │ ├── main.html │ └── partials/ │ ├── header.html │ ├── nav.html │ └── tc-header.html ├── examples/ │ ├── README.md │ ├── build.gradle │ ├── cucumber/ │ │ ├── build.gradle │ │ └── src/ │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── examples/ │ │ │ ├── CucumberTest.java │ │ │ └── Stepdefs.java │ │ └── resources/ │ │ ├── logback-test.xml │ │ └── org/ │ │ └── testcontainers/ │ │ └── examples/ │ │ └── is_search_possible.feature │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── hazelcast/ │ │ ├── build.gradle │ │ └── src/ │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── examples/ │ │ │ └── HazelcastTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── immudb/ │ │ ├── build.gradle │ │ └── src/ │ │ └── test/ │ │ ├── java/ │ │ │ └── ImmuDbTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── kafka-cluster/ │ │ ├── build.gradle │ │ └── src/ │ │ └── test/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ └── kafkacluster/ │ │ │ ├── ApacheKafkaContainerCluster.java │ │ │ ├── ApacheKafkaContainerClusterTest.java │ │ │ ├── ConfluentKafkaContainerCluster.java │ │ │ ├── ConfluentKafkaContainerClusterTest.java │ │ │ ├── KafkaContainerCluster.java │ │ │ ├── KafkaContainerClusterTest.java │ │ │ └── KafkaContainerKraftCluster.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── nats/ │ │ ├── build.gradle │ │ └── src/ │ │ └── test/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ └── NatsContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── neo4j-container/ │ │ ├── build.gradle │ │ └── src/ │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── containers/ │ │ │ └── Neo4jExampleTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── ollama-hugging-face/ │ │ ├── build.gradle │ │ └── src/ │ │ └── test/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ └── ollamahf/ │ │ │ ├── OllamaHuggingFaceContainer.java │ │ │ └── OllamaHuggingFaceTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── redis-backed-cache/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── mycompany/ │ │ │ └── cache/ │ │ │ ├── Cache.java │ │ │ └── RedisBackedCache.java │ │ └── test/ │ │ ├── java/ │ │ │ └── RedisBackedCacheTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── redis-backed-cache-testng/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── mycompany/ │ │ │ └── cache/ │ │ │ ├── Cache.java │ │ │ └── RedisBackedCache.java │ │ └── test/ │ │ ├── java/ │ │ │ └── RedisBackedCacheTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── selenium-container/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── example/ │ │ │ │ └── DemoApplication.java │ │ │ └── resources/ │ │ │ └── static/ │ │ │ └── foo.html │ │ └── test/ │ │ ├── java/ │ │ │ └── SeleniumContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── settings.gradle │ ├── sftp/ │ │ ├── build.gradle │ │ └── src/ │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── example/ │ │ │ └── SftpContainerTest.java │ │ └── resources/ │ │ ├── logback-test.xml │ │ ├── ssh_host_rsa_key │ │ ├── ssh_host_rsa_key.pub │ │ └── testcontainers/ │ │ └── file.txt │ ├── singleton-container/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ └── cache/ │ │ │ ├── Cache.java │ │ │ └── RedisBackedCache.java │ │ └── test/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ ├── AbstractIntegrationTest.java │ │ │ ├── BarConcreteTestClass.java │ │ │ └── FooConcreteTestClass.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── solr-container/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ ├── SearchEngine.java │ │ │ ├── SearchResult.java │ │ │ └── SolrSearchEngine.java │ │ └── test/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ └── SolrQueryTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── spring-boot/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── example/ │ │ │ │ ├── DemoApplication.java │ │ │ │ ├── DemoController.java │ │ │ │ ├── DemoEntity.java │ │ │ │ ├── DemoRepository.java │ │ │ │ └── DemoService.java │ │ │ └── resources/ │ │ │ └── application.yml │ │ └── test/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ ├── AbstractIntegrationTest.java │ │ │ └── DemoControllerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── spring-boot-kotlin-redis/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── main/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ └── redis/ │ │ │ ├── ExampleController.kt │ │ │ └── RedisApplication.kt │ │ └── test/ │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ └── redis/ │ │ │ ├── AbstractIntegrationTest.kt │ │ │ ├── RedisApplicationTests.kt │ │ │ └── RedisTest.kt │ │ └── resources/ │ │ └── logback-test.xml │ └── zookeeper/ │ ├── build.gradle │ └── src/ │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── example/ │ │ └── ZookeeperContainerTest.java │ └── resources/ │ └── logback-test.xml ├── gradle/ │ ├── ci-support.gradle │ ├── japicmp.gradle │ ├── publishing.gradle │ ├── shading.gradle │ ├── spotless.gradle │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── mkdocs.yml ├── modules/ │ ├── activemq/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── activemq/ │ │ │ ├── ActiveMQContainer.java │ │ │ └── ArtemisContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── activemq/ │ │ │ ├── ActiveMQContainerTest.java │ │ │ └── ArtemisContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── azure/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── azure/ │ │ │ │ ├── AzuriteContainer.java │ │ │ │ ├── EventHubsEmulatorContainer.java │ │ │ │ └── ServiceBusEmulatorContainer.java │ │ │ └── containers/ │ │ │ ├── CosmosDBEmulatorContainer.java │ │ │ └── KeyStoreBuilder.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── azure/ │ │ │ │ ├── AzuriteContainerTest.java │ │ │ │ ├── EventHubsEmulatorContainerTest.java │ │ │ │ └── ServiceBusEmulatorContainerTest.java │ │ │ └── containers/ │ │ │ └── CosmosDBEmulatorContainerTest.java │ │ └── resources/ │ │ ├── certificate.pem │ │ ├── eventhubs_config.json │ │ ├── key.pem │ │ ├── keystore.pfx │ │ ├── logback-test.xml │ │ └── service-bus-config.json │ ├── build.gradle │ ├── cassandra/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ ├── cassandra/ │ │ │ │ │ ├── CassandraContainer.java │ │ │ │ │ ├── CassandraDatabaseDelegate.java │ │ │ │ │ └── CassandraQueryWaitStrategy.java │ │ │ │ └── containers/ │ │ │ │ ├── CassandraContainer.java │ │ │ │ ├── delegate/ │ │ │ │ │ └── CassandraDatabaseDelegate.java │ │ │ │ └── wait/ │ │ │ │ └── CassandraQueryWaitStrategy.java │ │ │ └── resources/ │ │ │ └── cqlshrc │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── cassandra/ │ │ │ │ ├── CassandraContainerTest.java │ │ │ │ └── CompatibleCassandraImageTest.java │ │ │ └── containers/ │ │ │ ├── CassandraContainerTest.java │ │ │ └── CompatibleCassandraImageTest.java │ │ └── resources/ │ │ ├── cassandra-auth-required-configuration/ │ │ │ └── cassandra.yaml │ │ ├── cassandra-ssl-configuration/ │ │ │ ├── cassandra.cer │ │ │ ├── cassandra.yaml │ │ │ ├── keystore.p12 │ │ │ └── truststore.p12 │ │ ├── cassandra-test-configuration-example/ │ │ │ └── cassandra.yaml │ │ ├── client-ssl/ │ │ │ ├── cassandra.cer.pem │ │ │ └── cassandra.key.pem │ │ ├── initial-with-error.cql │ │ ├── initial.cql │ │ └── logback-test.xml │ ├── chromadb/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── chromadb/ │ │ │ └── ChromaDBContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── chromadb/ │ │ │ └── ChromaDBContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── clickhouse/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ ├── clickhouse/ │ │ │ │ │ ├── ClickHouseContainer.java │ │ │ │ │ ├── ClickHouseR2DBCDatabaseContainer.java │ │ │ │ │ └── ClickHouseR2DBCDatabaseContainerProvider.java │ │ │ │ └── containers/ │ │ │ │ ├── ClickHouseContainer.java │ │ │ │ └── ClickHouseProvider.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── services/ │ │ │ ├── org.testcontainers.containers.JdbcDatabaseContainerProvider │ │ │ └── org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── ClickhouseTestImages.java │ │ │ ├── clickhouse/ │ │ │ │ ├── ClickHouseContainerTest.java │ │ │ │ └── ClickHouseR2DBCDatabaseContainerTest.java │ │ │ ├── jdbc/ │ │ │ │ └── clickhouse/ │ │ │ │ └── ClickhouseJDBCDriverTest.java │ │ │ └── junit/ │ │ │ └── clickhouse/ │ │ │ └── SimpleClickhouseTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── cockroachdb/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ ├── cockroachdb/ │ │ │ │ │ └── CockroachContainer.java │ │ │ │ └── containers/ │ │ │ │ ├── CockroachContainer.java │ │ │ │ └── CockroachContainerProvider.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── services/ │ │ │ └── org.testcontainers.containers.JdbcDatabaseContainerProvider │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── CockroachDBTestImages.java │ │ │ ├── cockroachdb/ │ │ │ │ └── CockroachContainerTest.java │ │ │ └── jdbc/ │ │ │ └── cockroachdb/ │ │ │ └── CockroachDBJDBCDriverTest.java │ │ └── resources/ │ │ ├── logback-test.xml │ │ └── somepath/ │ │ └── init_postgresql.sql │ ├── consul/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── consul/ │ │ │ └── ConsulContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── consul/ │ │ │ └── ConsulContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── couchbase/ │ │ ├── AUTHORS │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── couchbase/ │ │ │ ├── BucketDefinition.java │ │ │ ├── CouchbaseContainer.java │ │ │ └── CouchbaseService.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── couchbase/ │ │ │ └── CouchbaseContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── cratedb/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ └── cratedb/ │ │ │ │ ├── CrateDBContainer.java │ │ │ │ └── CrateDBContainerProvider.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── services/ │ │ │ └── org.testcontainers.containers.JdbcDatabaseContainerProvider │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── CrateDBTestImages.java │ │ │ ├── jdbc/ │ │ │ │ └── cratedb/ │ │ │ │ └── CrateDBJDBCDriverTest.java │ │ │ └── junit/ │ │ │ └── cratedb/ │ │ │ └── SimpleCrateDBTest.java │ │ └── resources/ │ │ ├── logback-test.xml │ │ └── somepath/ │ │ └── init_cratedb.sql │ ├── database-commons/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── delegate/ │ │ │ │ ├── AbstractDatabaseDelegate.java │ │ │ │ └── DatabaseDelegate.java │ │ │ ├── exception/ │ │ │ │ └── ConnectionCreationException.java │ │ │ └── ext/ │ │ │ ├── ScriptScanner.java │ │ │ ├── ScriptSplitter.java │ │ │ └── ScriptUtils.java │ │ └── test/ │ │ └── java/ │ │ └── org/ │ │ └── testcontainers/ │ │ └── ext/ │ │ ├── ScriptScannerTest.java │ │ └── ScriptSplittingTest.java │ ├── databend/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ └── databend/ │ │ │ │ ├── DatabendContainer.java │ │ │ │ └── DatabendContainerProvider.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── services/ │ │ │ └── org.testcontainers.containers.JdbcDatabaseContainerProvider │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── databend/ │ │ │ ├── DatabendContainerTest.java │ │ │ └── DatabendJDBCDriverTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── db2/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ ├── containers/ │ │ │ │ │ ├── Db2Container.java │ │ │ │ │ └── Db2ContainerProvider.java │ │ │ │ └── db2/ │ │ │ │ └── Db2Container.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── services/ │ │ │ └── org.testcontainers.containers.JdbcDatabaseContainerProvider │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── Db2TestImages.java │ │ │ ├── db2/ │ │ │ │ └── Db2ContainerTest.java │ │ │ └── jdbc/ │ │ │ └── db2/ │ │ │ └── DB2JDBCDriverTest.java │ │ └── resources/ │ │ ├── container-license-acceptance.txt │ │ └── logback-test.xml │ ├── elasticsearch/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ └── elasticsearch/ │ │ │ │ └── ElasticsearchContainer.java │ │ │ └── resources/ │ │ │ └── elasticsearch-default-memory-vm.options │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── elasticsearch/ │ │ │ └── ElasticsearchContainerTest.java │ │ └── resources/ │ │ ├── http_ca.crt │ │ ├── logback-test.xml │ │ └── test-custom-memory-jvm.options │ ├── gcloud/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── containers/ │ │ │ │ ├── BigQueryEmulatorContainer.java │ │ │ │ ├── BigtableEmulatorContainer.java │ │ │ │ ├── DatastoreEmulatorContainer.java │ │ │ │ ├── FirestoreEmulatorContainer.java │ │ │ │ ├── PubSubEmulatorContainer.java │ │ │ │ └── SpannerEmulatorContainer.java │ │ │ └── gcloud/ │ │ │ ├── BigQueryEmulatorContainer.java │ │ │ ├── BigtableEmulatorContainer.java │ │ │ ├── DatastoreEmulatorContainer.java │ │ │ ├── FirestoreEmulatorContainer.java │ │ │ ├── PubSubEmulatorContainer.java │ │ │ └── SpannerEmulatorContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── gcloud/ │ │ │ ├── BigQueryEmulatorContainerTest.java │ │ │ ├── BigtableEmulatorContainerTest.java │ │ │ ├── DatastoreEmulatorContainerTest.java │ │ │ ├── FirestoreEmulatorContainerTest.java │ │ │ ├── PubSubEmulatorContainerTest.java │ │ │ └── SpannerEmulatorContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── grafana/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── grafana/ │ │ │ └── LgtmStackContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── grafana/ │ │ │ └── LgtmStackContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── hivemq/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── hivemq/ │ │ │ ├── HiveMQContainer.java │ │ │ ├── HiveMQExtension.java │ │ │ └── PathUtil.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── hivemq/ │ │ │ ├── ContainerWithControlCenterIT.java │ │ │ ├── ContainerWithCustomConfigIT.java │ │ │ ├── ContainerWithExtensionFromDirectoryIT.java │ │ │ ├── ContainerWithExtensionIT.java │ │ │ ├── ContainerWithExtensionSubclassIT.java │ │ │ ├── ContainerWithFileInExtensionHomeIT.java │ │ │ ├── ContainerWithFileInHomeIT.java │ │ │ ├── ContainerWithLicenseIT.java │ │ │ ├── ContainerWithoutPlatformExtensionsIT.java │ │ │ ├── CreateFileInCopiedDirectoryIT.java │ │ │ ├── CreateFileInExtensionDirectoryIT.java │ │ │ ├── DisableEnableExtensionFromDirectoryIT.java │ │ │ ├── DisableEnableExtensionIT.java │ │ │ ├── HiveMQExtensionTest.java │ │ │ ├── HiveMQTestContainerCore.java │ │ │ ├── PathUtilTest.java │ │ │ ├── docs/ │ │ │ │ ├── DemoDisableExtensionsIT.java │ │ │ │ ├── DemoExtensionTestsIT.java │ │ │ │ ├── DemoFilesIT.java │ │ │ │ └── DemoHiveMQContainerIT.java │ │ │ └── util/ │ │ │ ├── MyExtension.java │ │ │ ├── MyExtensionWithSubclasses.java │ │ │ ├── PublishModifier.java │ │ │ └── TestPublishModifiedUtil.java │ │ └── resources/ │ │ ├── additionalFile.txt │ │ ├── config.xml │ │ ├── inMemoryConfig.xml │ │ ├── logback-test.xml │ │ ├── modifier-extension/ │ │ │ ├── hivemq-extension.xml │ │ │ └── modifier-extension-1.0-SNAPSHOT.jar │ │ ├── modifier-extension-wrong-name/ │ │ │ ├── hivemq-extension.xml │ │ │ └── modifier-extension-1.0-SNAPSHOT.jar │ │ ├── myExtensionLicense.elic │ │ └── myLicense.lic │ ├── influxdb/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── containers/ │ │ │ └── InfluxDBContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── containers/ │ │ │ ├── InfluxDBContainerTest.java │ │ │ ├── InfluxDBContainerV1Test.java │ │ │ └── InfluxDBTestUtils.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── jdbc/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ ├── containers/ │ │ │ │ │ ├── JdbcDatabaseContainer.java │ │ │ │ │ └── JdbcDatabaseContainerProvider.java │ │ │ │ └── jdbc/ │ │ │ │ ├── ConnectionDelegate.java │ │ │ │ ├── ConnectionUrl.java │ │ │ │ ├── ConnectionWrapper.java │ │ │ │ ├── ContainerDatabaseDriver.java │ │ │ │ ├── ContainerLessJdbcDelegate.java │ │ │ │ ├── JdbcDatabaseDelegate.java │ │ │ │ └── ext/ │ │ │ │ └── ScriptUtils.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── services/ │ │ │ └── java.sql.Driver │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── containers/ │ │ │ │ └── JdbcDatabaseContainerTest.java │ │ │ └── jdbc/ │ │ │ ├── ConnectionUrlDriversTests.java │ │ │ ├── ConnectionUrlTest.java │ │ │ ├── ContainerDatabaseDriverTest.java │ │ │ ├── JdbcDatabaseDelegateTest.java │ │ │ └── MissingJdbcDriverTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── jdbc-test/ │ │ ├── build.gradle │ │ ├── sql/ │ │ │ └── init_mysql.sql │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── db/ │ │ │ │ └── AbstractContainerDatabaseTest.java │ │ │ └── jdbc/ │ │ │ └── AbstractJDBCDriverTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── junit-jupiter/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── junit/ │ │ │ └── jupiter/ │ │ │ ├── Container.java │ │ │ ├── DockerAvailableDetector.java │ │ │ ├── EnabledIfDockerAvailable.java │ │ │ ├── EnabledIfDockerAvailableCondition.java │ │ │ ├── FilesystemFriendlyNameGenerator.java │ │ │ ├── Testcontainers.java │ │ │ ├── TestcontainersExtension.java │ │ │ └── TestcontainersTestDescription.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── junit/ │ │ │ └── jupiter/ │ │ │ ├── ComposeContainerTests.java │ │ │ ├── DockerComposeContainerTests.java │ │ │ ├── EnabledIfDockerAvailableTests.java │ │ │ ├── FilesystemFriendlyNameGeneratorTest.java │ │ │ ├── JUnitJupiterTestImages.java │ │ │ ├── MetaAnnotationTest.java │ │ │ ├── MixedLifecycleTests.java │ │ │ ├── ParallelExecutionTests.java │ │ │ ├── PostgresContainerTests.java │ │ │ ├── TestLifecycleAwareContainerMock.java │ │ │ ├── TestLifecycleAwareExceptionCapturingTest.java │ │ │ ├── TestLifecycleAwareMethodTest.java │ │ │ ├── TestcontainersExtensionTests.java │ │ │ ├── TestcontainersNestedRestartedContainerTests.java │ │ │ ├── TestcontainersNestedSharedContainerTests.java │ │ │ ├── TestcontainersRestartBetweenTests.java │ │ │ ├── TestcontainersSharedContainerTests.java │ │ │ ├── WrongAnnotationUsageTests.java │ │ │ └── inheritance/ │ │ │ ├── AbstractTestBase.java │ │ │ ├── InheritedTests.java │ │ │ └── RedisContainer.java │ │ └── resources/ │ │ ├── docker-compose.yml │ │ └── logback-test.xml │ ├── k3s/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── k3s/ │ │ │ └── K3sContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── k3s/ │ │ │ ├── Fabric8K3sContainerTest.java │ │ │ ├── KubectlContainerTest.java │ │ │ └── OfficialClientK3sContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── k6/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── k6/ │ │ │ └── K6Container.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── k6/ │ │ │ └── K6ContainerTests.java │ │ └── resources/ │ │ ├── logback-test.xml │ │ └── scripts/ │ │ └── test.js │ ├── kafka/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── containers/ │ │ │ │ └── KafkaContainer.java │ │ │ └── kafka/ │ │ │ ├── ConfluentKafkaContainer.java │ │ │ ├── KafkaContainer.java │ │ │ └── KafkaHelper.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── AbstractKafka.java │ │ │ ├── KCatContainer.java │ │ │ ├── containers/ │ │ │ │ └── KafkaContainerTest.java │ │ │ └── kafka/ │ │ │ ├── CompatibleApacheKafkaImageTest.java │ │ │ ├── ConfluentKafkaContainerTest.java │ │ │ └── KafkaContainerTest.java │ │ └── resources/ │ │ ├── kafka_server_jaas.conf │ │ └── logback-test.xml │ ├── ldap/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── ldap/ │ │ │ └── LLdapContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── ldap/ │ │ │ └── LLdapContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── localstack/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── containers/ │ │ │ │ └── localstack/ │ │ │ │ └── LocalStackContainer.java │ │ │ └── localstack/ │ │ │ └── LocalStackContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── containers/ │ │ │ │ └── localstack/ │ │ │ │ ├── LegacyModeTest.java │ │ │ │ └── LocalstackTestImages.java │ │ │ └── localstack/ │ │ │ └── LocalStackContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── mariadb/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ ├── containers/ │ │ │ │ │ ├── MariaDBContainer.java │ │ │ │ │ ├── MariaDBContainerProvider.java │ │ │ │ │ ├── MariaDBR2DBCDatabaseContainer.java │ │ │ │ │ └── MariaDBR2DBCDatabaseContainerProvider.java │ │ │ │ └── mariadb/ │ │ │ │ ├── MariaDBContainer.java │ │ │ │ └── MariaDBR2DBCDatabaseContainer.java │ │ │ └── resources/ │ │ │ ├── META-INF/ │ │ │ │ └── services/ │ │ │ │ ├── org.testcontainers.containers.JdbcDatabaseContainerProvider │ │ │ │ └── org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider │ │ │ └── mariadb-default-conf/ │ │ │ └── my.cnf │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── MariaDBTestImages.java │ │ │ ├── containers/ │ │ │ │ └── MariaDBR2DBCDatabaseContainerTest.java │ │ │ ├── jdbc/ │ │ │ │ └── mariadb/ │ │ │ │ └── MariaDBJDBCDriverTest.java │ │ │ └── mariadb/ │ │ │ ├── MariaDBContainerTest.java │ │ │ └── MariaDBR2DBCDatabaseContainerTest.java │ │ └── resources/ │ │ ├── logback-test.xml │ │ └── somepath/ │ │ ├── init_mariadb.sql │ │ ├── init_unicode_mariadb.sql │ │ └── mariadb_conf_override/ │ │ └── my.cnf │ ├── milvus/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ └── milvus/ │ │ │ │ └── MilvusContainer.java │ │ │ └── resources/ │ │ │ └── testcontainers/ │ │ │ └── embedEtcd.yaml │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── milvus/ │ │ │ └── MilvusContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── minio/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── containers/ │ │ │ └── MinIOContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── containers/ │ │ │ └── MinIOContainerTest.java │ │ └── resources/ │ │ ├── logback-test.xml │ │ └── object_to_upload.txt │ ├── mockserver/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── containers/ │ │ │ │ └── MockServerContainer.java │ │ │ └── mockserver/ │ │ │ └── MockServerContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── mockserver/ │ │ │ └── MockServerContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── mongodb/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ ├── containers/ │ │ │ │ │ └── MongoDBContainer.java │ │ │ │ └── mongodb/ │ │ │ │ ├── MongoDBAtlasLocalContainer.java │ │ │ │ └── MongoDBContainer.java │ │ │ └── resources/ │ │ │ └── sharding.sh │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── mongodb/ │ │ │ ├── AbstractMongo.java │ │ │ ├── AtlasLocalDataAccess.java │ │ │ ├── CompatibleImageTest.java │ │ │ ├── MongoDBAtlasLocalContainerTest.java │ │ │ └── MongoDBContainerTest.java │ │ └── resources/ │ │ ├── atlas-local-index.json │ │ └── logback-test.xml │ ├── mssqlserver/ │ │ ├── AUTHORS │ │ ├── LICENSE │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ ├── containers/ │ │ │ │ │ ├── MSSQLR2DBCDatabaseContainer.java │ │ │ │ │ ├── MSSQLR2DBCDatabaseContainerProvider.java │ │ │ │ │ ├── MSSQLServerContainer.java │ │ │ │ │ └── MSSQLServerContainerProvider.java │ │ │ │ └── mssqlserver/ │ │ │ │ ├── MSSQLR2DBCDatabaseContainer.java │ │ │ │ └── MSSQLServerContainer.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── services/ │ │ │ ├── org.testcontainers.containers.JdbcDatabaseContainerProvider │ │ │ └── org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── MSSQLServerTestImages.java │ │ │ ├── containers/ │ │ │ │ └── MSSQLR2DBCDatabaseContainerTest.java │ │ │ ├── jdbc/ │ │ │ │ └── mssqlserver/ │ │ │ │ └── MSSQLServerJDBCDriverTest.java │ │ │ └── mssqlserver/ │ │ │ ├── CustomPasswordMSSQLServerTest.java │ │ │ ├── MSSQLR2DBCDatabaseContainerTest.java │ │ │ └── MSSQLServerContainerTest.java │ │ └── resources/ │ │ ├── container-license-acceptance.txt │ │ └── logback-test.xml │ ├── mysql/ │ │ ├── build.gradle │ │ ├── sql/ │ │ │ └── init_mysql.sql │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ ├── containers/ │ │ │ │ │ ├── MySQLContainer.java │ │ │ │ │ ├── MySQLContainerProvider.java │ │ │ │ │ ├── MySQLR2DBCDatabaseContainer.java │ │ │ │ │ └── MySQLR2DBCDatabaseContainerProvider.java │ │ │ │ └── mysql/ │ │ │ │ ├── MySQLContainer.java │ │ │ │ └── MySQLR2DBCDatabaseContainer.java │ │ │ └── resources/ │ │ │ ├── META-INF/ │ │ │ │ └── services/ │ │ │ │ ├── org.testcontainers.containers.JdbcDatabaseContainerProvider │ │ │ │ └── org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider │ │ │ └── mysql-default-conf/ │ │ │ └── my.cnf │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── MySQLTestImages.java │ │ │ ├── containers/ │ │ │ │ ├── MySQLR2DBCDatabaseContainerTest.java │ │ │ │ └── MySQLRootAccountTest.java │ │ │ ├── jdbc/ │ │ │ │ └── mysql/ │ │ │ │ ├── JDBCDriverWithPoolTest.java │ │ │ │ ├── MySQLDatabaseContainerDriverTest.java │ │ │ │ └── MySQLJDBCDriverTest.java │ │ │ └── mysql/ │ │ │ ├── MultiVersionMySQLTest.java │ │ │ ├── MySQLContainerTest.java │ │ │ └── MySQLR2DBCDatabaseContainerTest.java │ │ └── resources/ │ │ ├── logback-test.xml │ │ └── somepath/ │ │ ├── init_mysql.sql │ │ ├── init_unicode_mysql.sql │ │ └── mysql_conf_override/ │ │ └── my.cnf │ ├── neo4j/ │ │ ├── .gitignore │ │ ├── build.gradle │ │ └── src/ │ │ ├── custom-neo4j-plugin/ │ │ │ └── java/ │ │ │ └── ac/ │ │ │ └── simons/ │ │ │ └── neo4j/ │ │ │ └── demos/ │ │ │ └── plugins/ │ │ │ └── HelloWorld.java │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── containers/ │ │ │ │ └── Neo4jContainer.java │ │ │ └── neo4j/ │ │ │ └── Neo4jContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── neo4j/ │ │ │ └── Neo4jContainerTest.java │ │ └── resources/ │ │ ├── example-container-license-acceptance.txt │ │ └── logback-test.xml │ ├── nginx/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── containers/ │ │ │ │ └── NginxContainer.java │ │ │ └── nginx/ │ │ │ └── NginxContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── nginx/ │ │ │ └── NginxContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── oceanbase/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ └── oceanbase/ │ │ │ │ ├── OceanBaseCEContainer.java │ │ │ │ ├── OceanBaseCEContainerProvider.java │ │ │ │ └── OceanBaseJdbcUtils.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── services/ │ │ │ └── org.testcontainers.containers.JdbcDatabaseContainerProvider │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── oceanbase/ │ │ │ ├── OceanBaseJdbcDriverTest.java │ │ │ └── SimpleOceanBaseCETest.java │ │ └── resources/ │ │ ├── init.sql │ │ └── logback-test.xml │ ├── ollama/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── ollama/ │ │ │ └── OllamaContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── ollama/ │ │ │ └── OllamaContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── openfga/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── openfga/ │ │ │ └── OpenFGAContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── openfga/ │ │ │ └── OpenFGAContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── oracle-free/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ └── oracle/ │ │ │ │ ├── OracleContainer.java │ │ │ │ ├── OracleContainerProvider.java │ │ │ │ ├── OracleR2DBCDatabaseContainer.java │ │ │ │ └── OracleR2DBCDatabaseContainerProvider.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── services/ │ │ │ ├── org.testcontainers.containers.JdbcDatabaseContainerProvider │ │ │ └── org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── junit/ │ │ │ │ └── oracle/ │ │ │ │ └── SimpleOracleTest.java │ │ │ └── oracle/ │ │ │ ├── jdbc/ │ │ │ │ └── OracleJDBCDriverTest.java │ │ │ └── r2dbc/ │ │ │ └── OracleR2DBCDatabaseContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── oracle-xe/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ └── containers/ │ │ │ │ ├── OracleContainer.java │ │ │ │ ├── OracleContainerProvider.java │ │ │ │ ├── OracleR2DBCDatabaseContainer.java │ │ │ │ └── OracleR2DBCDatabaseContainerProvider.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── services/ │ │ │ ├── org.testcontainers.containers.JdbcDatabaseContainerProvider │ │ │ └── org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── containers/ │ │ │ │ ├── jdbc/ │ │ │ │ │ └── OracleJDBCDriverTest.java │ │ │ │ └── r2dbc/ │ │ │ │ └── OracleR2DBCDatabaseContainerTest.java │ │ │ └── junit/ │ │ │ └── oracle/ │ │ │ └── SimpleOracleTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── orientdb/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── containers/ │ │ │ │ └── OrientDBContainer.java │ │ │ └── orientdb/ │ │ │ └── OrientDBContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── orientdb/ │ │ │ └── OrientDBContainerTest.java │ │ └── resources/ │ │ ├── initscript.osql │ │ ├── logback-test.xml │ │ └── orientdb-server-config.xml │ ├── pinecone/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── pinecone/ │ │ │ └── PineconeLocalContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── pinecone/ │ │ │ └── PineconeLocalContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── postgresql/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ ├── containers/ │ │ │ │ │ ├── PgVectorContainerProvider.java │ │ │ │ │ ├── PostgisContainerProvider.java │ │ │ │ │ ├── PostgreSQLContainer.java │ │ │ │ │ ├── PostgreSQLContainerProvider.java │ │ │ │ │ ├── PostgreSQLR2DBCDatabaseContainer.java │ │ │ │ │ ├── PostgreSQLR2DBCDatabaseContainerProvider.java │ │ │ │ │ └── TimescaleDBContainerProvider.java │ │ │ │ └── postgresql/ │ │ │ │ ├── PostgreSQLContainer.java │ │ │ │ └── PostgreSQLR2DBCDatabaseContainer.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── services/ │ │ │ ├── org.testcontainers.containers.JdbcDatabaseContainerProvider │ │ │ └── org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── PostgreSQLTestImages.java │ │ │ ├── containers/ │ │ │ │ ├── PostgreSQLConnectionURLTest.java │ │ │ │ ├── PostgreSQLR2DBCDatabaseContainerTest.java │ │ │ │ └── TimescaleDBContainerTest.java │ │ │ ├── jdbc/ │ │ │ │ ├── DatabaseDriverShutdownTest.java │ │ │ │ ├── DatabaseDriverTmpfsTest.java │ │ │ │ ├── pgvector/ │ │ │ │ │ └── PgVectorJDBCDriverTest.java │ │ │ │ ├── postgis/ │ │ │ │ │ └── PostgisJDBCDriverTest.java │ │ │ │ ├── postgresql/ │ │ │ │ │ └── PostgreSQLJDBCDriverTest.java │ │ │ │ └── timescaledb/ │ │ │ │ └── TimescaleDBJDBCDriverTest.java │ │ │ └── postgresql/ │ │ │ ├── CompatibleImageTest.java │ │ │ ├── PostgreSQLContainerTest.java │ │ │ └── PostgreSQLR2DBCDatabaseContainerTest.java │ │ └── resources/ │ │ ├── logback-test.xml │ │ └── somepath/ │ │ ├── init_postgresql.sql │ │ ├── init_postgresql_2.sql │ │ └── init_timescaledb.sql │ ├── presto/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ └── containers/ │ │ │ │ ├── PrestoContainer.java │ │ │ │ └── PrestoContainerProvider.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── services/ │ │ │ └── org.testcontainers.containers.JdbcDatabaseContainerProvider │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── PrestoTestImages.java │ │ │ ├── containers/ │ │ │ │ └── PrestoContainerTest.java │ │ │ └── jdbc/ │ │ │ └── presto/ │ │ │ └── PrestoJDBCDriverTest.java │ │ └── resources/ │ │ ├── initial.sql │ │ └── logback-test.xml │ ├── pulsar/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── containers/ │ │ │ │ └── PulsarContainer.java │ │ │ └── pulsar/ │ │ │ └── PulsarContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── pulsar/ │ │ │ ├── AbstractPulsar.java │ │ │ ├── CompatibleApachePulsarImageTest.java │ │ │ └── PulsarContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── qdrant/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── qdrant/ │ │ │ └── QdrantContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── qdrant/ │ │ │ └── QdrantContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── questdb/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ └── containers/ │ │ │ │ ├── LegacyQuestDBProvider.java │ │ │ │ ├── QuestDBContainer.java │ │ │ │ └── QuestDBProvider.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── services/ │ │ │ └── org.testcontainers.containers.JdbcDatabaseContainerProvider │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── QuestDBTestImages.java │ │ │ ├── jdbc/ │ │ │ │ └── questdb/ │ │ │ │ └── QuestDBJDBCDriverTest.java │ │ │ └── junit/ │ │ │ └── questdb/ │ │ │ └── SimpleQuestDBTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── r2dbc/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ └── r2dbc/ │ │ │ │ ├── CancellableSubscription.java │ │ │ │ ├── ConnectionPublisher.java │ │ │ │ ├── EmptySubscription.java │ │ │ │ ├── Hidden.java │ │ │ │ ├── R2DBCDatabaseContainer.java │ │ │ │ ├── R2DBCDatabaseContainerProvider.java │ │ │ │ └── TestcontainersR2DBCConnectionFactory.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── services/ │ │ │ └── io.r2dbc.spi.ConnectionFactoryProvider │ │ ├── test/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ └── r2dbc/ │ │ │ │ └── TestcontainersR2DBCConnectionFactoryTest.java │ │ │ └── resources/ │ │ │ └── logback-test.xml │ │ └── testFixtures/ │ │ └── java/ │ │ └── org/ │ │ └── testcontainers/ │ │ └── r2dbc/ │ │ └── AbstractR2DBCDatabaseContainerTest.java │ ├── rabbitmq/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── containers/ │ │ │ │ └── RabbitMQContainer.java │ │ │ └── rabbitmq/ │ │ │ └── RabbitMQContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── rabbitmq/ │ │ │ ├── RabbitMQContainerTest.java │ │ │ └── RabbitMQTestImages.java │ │ └── resources/ │ │ ├── certs/ │ │ │ ├── ca_certificate.pem │ │ │ ├── ca_key.pem │ │ │ ├── client_certificate.pem │ │ │ ├── client_key.p12 │ │ │ ├── client_key.pem │ │ │ ├── server_certificate.pem │ │ │ ├── server_key.p12 │ │ │ ├── server_key.pem │ │ │ └── truststore.jks │ │ ├── logback-test.xml │ │ ├── rabbitmq-custom.conf │ │ └── rabbitmq-custom.config │ ├── redpanda/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ └── redpanda/ │ │ │ │ └── RedpandaContainer.java │ │ │ └── resources/ │ │ │ └── testcontainers/ │ │ │ ├── bootstrap.yaml.ftl │ │ │ ├── entrypoint-tc.sh │ │ │ └── redpanda.yaml.ftl │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── redpanda/ │ │ │ ├── AbstractRedpanda.java │ │ │ ├── CompatibleImageTest.java │ │ │ └── RedpandaContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── scylladb/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── scylladb/ │ │ │ └── ScyllaDBContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── scylladb/ │ │ │ └── ScyllaDBContainerTest.java │ │ └── resources/ │ │ ├── keys/ │ │ │ ├── node0.cer │ │ │ ├── node0.p12 │ │ │ ├── scylla.cer.pem │ │ │ ├── scylla.key.pem │ │ │ ├── scylla.keystore │ │ │ └── scylla.truststore │ │ ├── logback-test.xml │ │ └── scylla-test-ssl/ │ │ └── scylla.yaml │ ├── selenium/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── containers/ │ │ │ │ ├── BrowserWebDriverContainer.java │ │ │ │ ├── DefaultRecordingFileFactory.java │ │ │ │ ├── RecordingFileFactory.java │ │ │ │ └── SeleniumUtils.java │ │ │ └── selenium/ │ │ │ └── BrowserWebDriverContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── containers/ │ │ │ │ └── DefaultRecordingFileFactoryTest.java │ │ │ ├── junit/ │ │ │ │ ├── SeleniumStartTest.java │ │ │ │ └── SeleniumUtilsTest.java │ │ │ └── selenium/ │ │ │ ├── BaseWebDriverContainerTest.java │ │ │ ├── BrowserWebDriverContainerTest.java │ │ │ ├── ChromeRecordingWebDriverContainerTest.java │ │ │ ├── ChromeWebDriverContainerTest.java │ │ │ ├── ContainerWithoutCapabilitiesTest.java │ │ │ ├── CustomWaitTimeoutWebDriverContainerTest.java │ │ │ ├── EdgeWebDriverContainerTest.java │ │ │ ├── FirefoxWebDriverContainerTest.java │ │ │ ├── LocalServerWebDriverContainerTest.java │ │ │ └── SpecificImageNameWebDriverContainerTest.java │ │ └── resources/ │ │ ├── logback-test.xml │ │ ├── manifests/ │ │ │ ├── MANIFEST-2.45.0.MF │ │ │ └── MANIFEST-3.5.2.MF │ │ └── server/ │ │ └── index.html │ ├── solace/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── solace/ │ │ │ ├── Service.java │ │ │ └── SolaceContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── solace/ │ │ │ ├── SolaceContainerAMQPTest.java │ │ │ ├── SolaceContainerMQTTTest.java │ │ │ ├── SolaceContainerRESTTest.java │ │ │ └── SolaceContainerSMFTest.java │ │ └── resources/ │ │ ├── client.pfx │ │ ├── logback-test.xml │ │ ├── rootCA.crt │ │ ├── solace.pem │ │ └── truststore │ ├── solr/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── containers/ │ │ │ │ ├── SolrClientUtils.java │ │ │ │ ├── SolrClientUtilsException.java │ │ │ │ ├── SolrContainer.java │ │ │ │ └── SolrContainerConfiguration.java │ │ │ └── solr/ │ │ │ └── SolrContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── solr/ │ │ │ └── SolrContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── spock/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── groovy/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── spock/ │ │ │ ├── DockerAvailableDetector.groovy │ │ │ ├── SpockTestDescription.groovy │ │ │ ├── Testcontainers.groovy │ │ │ ├── TestcontainersExtension.groovy │ │ │ └── TestcontainersMethodInterceptor.groovy │ │ └── test/ │ │ ├── groovy/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── spock/ │ │ │ ├── ComposeContainerIT.groovy │ │ │ ├── DockerComposeContainerIT.groovy │ │ │ ├── MySqlContainerIT.groovy │ │ │ ├── PostgresContainerIT.groovy │ │ │ ├── SharedComposeContainerIT.groovy │ │ │ ├── SharedDockerComposeContainerIT.groovy │ │ │ ├── SpockTestImages.groovy │ │ │ ├── TestHierarchyIT.groovy │ │ │ ├── TestLifecycleAwareContainerMock.java │ │ │ ├── TestLifecycleAwareIT.groovy │ │ │ ├── TestcontainersExtensionTest.groovy │ │ │ ├── TestcontainersRestartBetweenTestsIT.groovy │ │ │ └── TestcontainersSharedContainerIT.groovy │ │ └── resources/ │ │ ├── docker-compose.yml │ │ └── logback-test.xml │ ├── tidb/ │ │ ├── build.gradle │ │ ├── sql/ │ │ │ └── init_mysql.sql │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ └── tidb/ │ │ │ │ ├── TiDBContainer.java │ │ │ │ └── TiDBContainerProvider.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── services/ │ │ │ └── org.testcontainers.containers.JdbcDatabaseContainerProvider │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── TiDBTestImages.java │ │ │ ├── jdbc/ │ │ │ │ └── tidb/ │ │ │ │ └── TiDBJDBCDriverTest.java │ │ │ └── tidb/ │ │ │ └── TiDBContainerTest.java │ │ └── resources/ │ │ ├── logback-test.xml │ │ └── somepath/ │ │ └── init_tidb.sql │ ├── timeplus/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ └── timeplus/ │ │ │ │ ├── TimeplusContainer.java │ │ │ │ └── TimeplusContainerProvider.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── services/ │ │ │ └── org.testcontainers.containers.JdbcDatabaseContainerProvider │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── TimeplusImages.java │ │ │ ├── junit/ │ │ │ │ └── timeplus/ │ │ │ │ └── TimeplusJDBCDriverTest.java │ │ │ └── timeplus/ │ │ │ └── TimeplusContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── toxiproxy/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── containers/ │ │ │ │ └── ToxiproxyContainer.java │ │ │ └── toxiproxy/ │ │ │ └── ToxiproxyContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── toxiproxy/ │ │ │ └── ToxiproxyContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── trino/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── testcontainers/ │ │ │ │ ├── containers/ │ │ │ │ │ ├── TrinoContainer.java │ │ │ │ │ └── TrinoContainerProvider.java │ │ │ │ └── trino/ │ │ │ │ └── TrinoContainer.java │ │ │ └── resources/ │ │ │ └── META-INF/ │ │ │ └── services/ │ │ │ └── org.testcontainers.containers.JdbcDatabaseContainerProvider │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ ├── TrinoTestImages.java │ │ │ ├── jdbc/ │ │ │ │ └── trino/ │ │ │ │ └── TrinoJDBCDriverTest.java │ │ │ └── trino/ │ │ │ └── TrinoContainerTest.java │ │ └── resources/ │ │ ├── initial.sql │ │ └── logback-test.xml │ ├── typesense/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── typesense/ │ │ │ └── TypesenseContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── typesense/ │ │ │ └── TypesenseContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── vault/ │ │ ├── AUTHORS │ │ ├── LICENSE │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── vault/ │ │ │ ├── VaultContainer.java │ │ │ └── VaultLogLevel.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── vault/ │ │ │ ├── VaultClientTest.java │ │ │ └── VaultContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── weaviate/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── weaviate/ │ │ │ └── WeaviateContainer.java │ │ └── test/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── weaviate/ │ │ │ └── WeaviateContainerTest.java │ │ └── resources/ │ │ └── logback-test.xml │ └── yugabytedb/ │ ├── build.gradle │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── testcontainers/ │ │ │ └── containers/ │ │ │ ├── YugabyteDBYCQLContainer.java │ │ │ ├── YugabyteDBYSQLContainer.java │ │ │ ├── YugabyteDBYSQLContainerProvider.java │ │ │ ├── delegate/ │ │ │ │ ├── AbstractYCQLDelegate.java │ │ │ │ └── YugabyteDBYCQLDelegate.java │ │ │ └── strategy/ │ │ │ ├── YugabyteDBYCQLWaitStrategy.java │ │ │ └── YugabyteDBYSQLWaitStrategy.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── services/ │ │ └── org.testcontainers.containers.JdbcDatabaseContainerProvider │ └── test/ │ ├── java/ │ │ └── org/ │ │ └── testcontainers/ │ │ ├── jdbc/ │ │ │ └── yugabytedb/ │ │ │ └── YugabyteDBYSQLJDBCDriverTest.java │ │ └── junit/ │ │ └── yugabytedb/ │ │ ├── YugabyteDBYCQLTest.java │ │ └── YugabyteDBYSQLTest.java │ └── resources/ │ ├── init/ │ │ └── init_yql.sql │ └── logback-test.xml ├── requirements.txt ├── runtime.txt ├── settings.gradle ├── smoke-test/ │ ├── build.gradle │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle │ └── turbo-mode/ │ ├── build.gradle │ └── src/ │ └── test/ │ ├── java/ │ │ └── org/ │ │ └── testcontainers/ │ │ └── example/ │ │ ├── AbstractRedisContainer.java │ │ ├── RedisContainer1Test.java │ │ ├── RedisContainer2Test.java │ │ ├── RedisContainer3Test.java │ │ └── RedisContainer4Test.java │ └── resources/ │ └── logback-test.xml └── test-support/ ├── build.gradle └── src/ ├── main/ │ └── java/ │ └── org/ │ └── testcontainers/ │ └── testsupport/ │ ├── Flaky.java │ └── FlakyTestJUnit4RetryRule.java └── test/ ├── java/ │ └── org/ │ └── testcontainers/ │ └── testsupport/ │ └── FlakyRuleTest.java └── resources/ └── logback-test.xml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .circleci/config.yml ================================================ version: 2 jobs: minimal_core: machine: enabled: true steps: - checkout - run: command: ./gradlew --no-daemon --continue --scan testcontainers:test --tests '*GenericContainerRuleTest' - run: name: Save test results command: | mkdir -p ~/junit/ find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/junit/ \; when: always - store_test_results: path: ~/junit - store_artifacts: path: ~/junit workflows: version: 2 test_all: jobs: - minimal_core ================================================ FILE: .devcontainer/devcontainer.json ================================================ // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: // https://github.com/microsoft/vscode-dev-containers/tree/v0.241.1/containers/java-8 { "name": "Java 17", "image": "mcr.microsoft.com/devcontainers/java:0-17", // Configure tool-specific properties. "customizations": { // Configure properties specific to VS Code. "vscode": { // Add the IDs of extensions you want installed when the container is created. "extensions": [ "vscjava.vscode-java-pack" ] } }, // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. // "postCreateCommand": "java -version", // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "vscode", "features": { "ghcr.io/devcontainers/features/git:1": {}, "ghcr.io/devcontainers/features/github-cli:1": {}, "ghcr.io/meaningful-ooo/devcontainer-features/homebrew:2": {}, "ghcr.io/devcontainers/features/java:1": { "version": "none", "installMaven": "false", "installGradle": "false" }, "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}, "ghcr.io/devcontainers/features/node:1": {}, "ghcr.io/devcontainers/features/sshd:1": { "version": "latest" } }, "postStartCommand": ["./gradlew", "compileJava"] } ================================================ FILE: .editorconfig ================================================ # EditorConfig is awesome: http://EditorConfig.org # top-most EditorConfig file root = true # Unix-style newlines with a newline ending every file [*] end_of_line = lf insert_final_newline = true charset = utf-8 indent_style = space indent_size = 4 trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false [*.java] indent_style = space indent_size = 4 # Never use star imports ij_java_names_count_to_use_import_on_demand = 99 ij_java_class_count_to_use_import_on_demand = 99 ij_java_layout_static_imports_separately = true [*.{yml, yaml}] indent_size = 2 ij_yaml_keep_indents_on_empty_lines = false ================================================ FILE: .gitattributes ================================================ *.sh text eol=lf ================================================ FILE: .github/CODEOWNERS ================================================ * @testcontainers/java-team ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [testcontainers] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yaml ================================================ name: Bug Report description: File a bug report title: "[Bug]: " labels: ["type/bug"] body: - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report! Before submitting a `bug`, please make sure there is no existing issue for the one you encountered and it has been discussed with the team via [discussions](https://github.com/testcontainers/testcontainers-java/discussions) or Slack. - type: dropdown id: module attributes: label: Module description: Which Testcontainers module are you using? options: - Core - ActiveMQ - Azure - Cassandra - ChromaDB - Clickhouse - CockroachDB - Consul - Couchbase - CrateDB - Databend - DB2 - Dynalite - Elasticsearch - GCloud - Grafana - HiveMQ - InfluxDB - K3S - K6 - Kafka - LDAP - LocalStack - MariaDB - Milvus - MinIO - MockServer - MongoDB - MSSQLServer - MySQL - Neo4j - NGINX - OceanBase - Ollama - OpenFGA - Oracle Free - Oracle XE - OrientDB - Pinecone - PostgreSQL - Presto - Pulsar - Qdrant - QuestDB - RabbitMQ - Redpanda - ScyllaDB - Selenium - Solace - Solr - TiDB - Timeplus - ToxiProxy - Trino - Typesense - Vault - Weaviate - YugabyteDB validations: required: true - type: input id: tc-version attributes: label: Testcontainers version description: Which Testcontainers version are you using? placeholder: ex. 1.17.2 validations: required: true - type: dropdown id: latest-version attributes: label: Using the latest Testcontainers version? description: If you are not using the latest version, can you update your project and try to reproduce the issue? Is it still happening? options: - 'Yes' - 'No' validations: required: true - type: input id: host-os attributes: label: Host OS description: Which Operating System are you using? placeholder: e.g. Linux, Windows validations: required: true - type: input id: host-arch attributes: label: Host Arch description: Which architecture are you using? placeholder: e.g. x86, ARM validations: required: true - type: textarea id: docker-version attributes: label: Docker version description: Please run `docker version` and copy and paste the output into this field. render: shell validations: required: true - type: textarea id: what-happened attributes: label: What happened? description: Provide the context and the expected result. validations: required: true - type: textarea id: logs attributes: label: Relevant log output description: Please copy and paste any relevant log output. The content will be automatically formatted as code, so no need for backticks. render: shell - type: textarea id: additional-information attributes: label: Additional Information description: | Any links or references to have more context about the issue. Tip: You can attach a minimal sample project to reproduce the issue or provide further log files by clicking into this area to focus it and then dragging files in. ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Need help or have a question? url: https://slack.testcontainers.org/ about: Visit our slack channel. - name: Have a question or want to drive a Community conversation? url: https://github.com/testcontainers/testcontainers-java/discussions/ about: Visit our Discussions page. ================================================ FILE: .github/ISSUE_TEMPLATE/enhancement.yaml ================================================ name: Enhancement description: Suggest an enhancement title: "[Enhancement]: " labels: ["type/enhancement"] body: - type: markdown attributes: value: | Before submitting an `enhancement`, please make sure there is no existing enhancement for the one you are requesting and it has been discussed with the team via [discussions](https://github.com/testcontainers/testcontainers-java/discussions) or Slack. If so, please provide the following information: - type: dropdown id: module attributes: label: Module description: For which Testcontainers module does the enhancement proposal apply? options: - Core - ActiveMQ - Azure - Cassandra - ChromaDB - Clickhouse - CockroachDB - Consul - Couchbase - CrateDB - Databend - DB2 - Dynalite - Elasticsearch - GCloud - Grafana - HiveMQ - InfluxDB - K3S - K6 - Kafka - LDAP - LocalStack - MariaDB - Milvus - MinIO - MockServer - MongoDB - MSSQLServer - MySQL - Neo4j - NGINX - OceanBase - Ollama - OpenFGA - Oracle Free - Oracle XE - OrientDB - Pinecone - PostgreSQL - Presto - Pulsar - Qdrant - QuestDB - RabbitMQ - Redpanda - ScyllaDB - Selenium - Solace - Solr - TiDB - Timeplus - ToxiProxy - Trino - Typesense - Vault - Weaviate - YugabyteDB validations: required: true - type: textarea id: proposal attributes: label: Proposal description: What should be improved? What are the limitations of the current implications that would be solved by the proposal? validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/feature.yaml ================================================ name: Feature description: Suggest a new feature title: "[Feature]: " labels: ["type/feature"] body: - type: markdown attributes: value: | Before submitting a `feature`, please make sure there is no existing feature for the one you are requesting and it has been discussed with the team via [discussions](https://github.com/testcontainers/testcontainers-java/discussions) or Slack. If so, please provide the following information: - type: dropdown id: module attributes: label: Module description: Is this feature related to any of the existing modules? options: - Core - ActiveMQ - Azure - Cassandra - ChromaDB - Clickhouse - CockroachDB - CrateDB - Consul - Couchbase - Databend - DB2 - Dynalite - Elasticsearch - GCloud - Grafana - HiveMQ - InfluxDB - K3S - K6 - Kafka - LDAP - LocalStack - MariaDB - Milvus - MinIO - MockServer - MongoDB - MSSQLServer - MySQL - Neo4j - NGINX - OceanBase - Ollama - OpenFGA - Oracle Free - Oracle XE - OrientDB - Pinecone - PostgreSQL - Qdrant - QuestDB - Presto - Pulsar - RabbitMQ - Redpanda - ScyllaDB - Selenium - Solace - Solr - TiDB - Timeplus - ToxiProxy - Trino - Typesense - Vault - Weaviate - YugabyteDB - New Module - type: textarea id: problem attributes: label: Problem description: Is this feature related to a problem? Please describe it. validations: required: true - type: textarea id: solution attributes: label: Solution description: What's the proposed solution for this feature? validations: required: true - type: textarea id: benefit attributes: label: Benefit description: What's the benefit of adding this feature to the project? validations: required: true - type: textarea id: alternatives attributes: label: Alternatives description: Are there other alternatives? Please describe them. validations: required: true - type: dropdown id: contribute attributes: label: Would you like to help contributing this feature? options: - 'Yes' - 'No' validations: required: true ================================================ FILE: .github/actions/setup-build/action.yml ================================================ name: Set up Build description: Sets up Build inputs: java-version: description: 'The Java version to set up' required: true default: '17' runs: using: "composite" steps: - uses: ./.github/actions/setup-java with: java-version: ${{ inputs.java-version }} - name: Clear existing docker image cache shell: bash run: docker image prune -af - uses: ./.github/actions/setup-gradle ================================================ FILE: .github/actions/setup-gradle/action.yml ================================================ name: Set up Gradle Action description: Sets up Gradle Action runs: using: "composite" steps: - name: Setup Gradle Build Action uses: gradle/actions/setup-gradle@v4 with: gradle-home-cache-includes: | caches notifications jdks ================================================ FILE: .github/actions/setup-java/action.yml ================================================ name: Set up Java description: Sets up Java version inputs: java-version: description: 'The Java version to set up' required: true default: '17' runs: using: "composite" steps: - uses: actions/setup-java@v4 with: java-version: ${{ inputs.java-version }} distribution: temurin ================================================ FILE: .github/actions/setup-junit-report/action.yml ================================================ name: Set up JUnit Report description: Sets up JUnit Report runs: using: "composite" steps: - name: Publish Test Report uses: mikepenz/action-junit-report@v4 if: always() # always run even if the previous step fails with: report_paths: '**/build/test-results/test/TEST-*.xml' annotate_only: true ================================================ FILE: .github/bumper.yml ================================================ updates: - path: mkdocs.yml pattern: 'latest_version: (.*)' ================================================ FILE: .github/dependabot.yml ================================================ version: 2 registries: gradle-plugin-portal: type: maven-repository url: https://plugins.gradle.org/m2 username: dummy # Required by dependabot password: dummy # Required by dependabot updates: - package-ecosystem: "gradle" directory: "/core" schedule: interval: "weekly" open-pull-requests-limit: 10 ignore: - dependency-name: "org.slf4j:slf4j-api" update-types: [ "version-update:semver-major" ] - dependency-name: "org.mockito:mockito-core" update-types: [ "version-update:semver-major" ] - dependency-name: "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" update-types: [ "version-update:semver-minor", "version-update:semver-patch" ] - dependency-name: "org.junit.jupiter:junit-jupiter" update-types: [ "version-update:semver-major" ] - dependency-name: "org.junit.platform:junit-platform-launcher" update-types: [ "version-update:semver-major" ] - package-ecosystem: "gradle" directory: "/" allow: - dependency-name: "com.gradle*" registries: - gradle-plugin-portal schedule: interval: "weekly" open-pull-requests-limit: 10 ignore: - dependency-name: "com.gradleup.shadow" update-types: [ "version-update:semver-major" ] - dependency-name: "org.junit.jupiter:junit-jupiter" update-types: [ "version-update:semver-major" ] - dependency-name: "org.junit.platform:junit-platform-launcher" update-types: [ "version-update:semver-major" ] # Explicit entry for each module - package-ecosystem: "gradle" directory: "/modules/activemq" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/azure" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/cassandra" schedule: interval: "monthly" open-pull-requests-limit: 10 ignore: - dependency-name: "io.dropwizard.metrics:metrics-core" update-types: [ "version-update:semver-major" ] - package-ecosystem: "gradle" directory: "/modules/chromadb" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/clickhouse" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/cockroachdb" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/consul" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/couchbase" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/cratedb" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/database-commons" schedule: interval: "monthly" - package-ecosystem: "gradle" directory: "/modules/databend" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/db2" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/dynalite" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/elasticsearch" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/gcloud" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/grafana" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/hivemq" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/influxdb" schedule: interval: "monthly" open-pull-requests-limit: 10 ignore: - dependency-name: "com.influxdb:influxdb-java-client" update-types: [ "version-update:semver-major" ] - package-ecosystem: "gradle" directory: "/modules/jdbc" schedule: interval: "monthly" open-pull-requests-limit: 10 ignore: - dependency-name: "org.mockito:mockito-core" update-types: [ "version-update:semver-major" ] - package-ecosystem: "gradle" directory: "/modules/jdbc-test" schedule: interval: "monthly" open-pull-requests-limit: 10 ignore: - dependency-name: "org.apache.tomcat:tomcat-jdbc" update-types: [ "version-update:semver-minor" ] - dependency-name: "org.junit.jupiter:junit-jupiter" update-types: [ "version-update:semver-major" ] - package-ecosystem: "gradle" directory: "/modules/junit-jupiter" schedule: interval: "monthly" open-pull-requests-limit: 10 ignore: - dependency-name: "org.mockito:mockito-core" update-types: [ "version-update:semver-major" ] - dependency-name: "org.junit:junit-bom" update-types: [ "version-update:semver-major" ] - package-ecosystem: "gradle" directory: "/modules/k3s" schedule: interval: "monthly" open-pull-requests-limit: 10 ignore: - dependency-name: "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml" update-types: [ "version-update:semver-minor", "version-update:semver-patch" ] - package-ecosystem: "gradle" directory: "/modules/k6" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/kafka" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/ldap" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/localstack" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/mariadb" schedule: interval: "monthly" open-pull-requests-limit: 10 ignore: - dependency-name: "org.mariadb:r2dbc-mariadb" update-types: [ "version-update:semver-minor" ] - package-ecosystem: "gradle" directory: "/modules/milvus" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/minio" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/mockserver" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/mongodb" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/mssqlserver" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/mysql" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/neo4j" schedule: interval: "monthly" open-pull-requests-limit: 10 ignore: - dependency-name: "org.neo4j.driver:neo4j-java-driver" update-types: [ "version-update:semver-major" ] - dependency-name: "org.neo4j:neo4j" update-types: [ "version-update:semver-major" ] - package-ecosystem: "gradle" directory: "/modules/nginx" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/oceanbase" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/ollama" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/openfga" schedule: interval: "monthly" - package-ecosystem: "gradle" directory: "/modules/oracle-free" schedule: interval: "monthly" - package-ecosystem: "gradle" directory: "/modules/oracle-xe" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/orientdb" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/postgresql" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/presto" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/pinecone" schedule: interval: "monthly" open-pull-requests-limit: 10 ignore: - dependency-name: "io.pinecone:pinecone-client" update-types: [ "version-update:semver-major" ] - package-ecosystem: "gradle" directory: "/modules/pulsar" schedule: interval: "monthly" open-pull-requests-limit: 10 ignore: - dependency-name: "org.apache.pulsar:pulsar-bom" update-types: [ "version-update:semver-patch" ] - package-ecosystem: "gradle" directory: "/modules/qdrant" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/questdb" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/r2dbc" schedule: interval: "monthly" open-pull-requests-limit: 10 ignore: - dependency-name: "io.r2dbc:r2dbc-spi" update-types: [ "version-update:semver-major", "version-update:semver-minor" ] - dependency-name: "org.junit.jupiter:junit-jupiter" update-types: [ "version-update:semver-major" ] - package-ecosystem: "gradle" directory: "/modules/rabbitmq" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/redpanda" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/scylladb" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/selenium" schedule: interval: "monthly" open-pull-requests-limit: 10 ignore: - dependency-name: "org.seleniumhq.selenium:selenium-bom" update-types: [ "version-update:semver-minor" ] - package-ecosystem: "gradle" directory: "/modules/solace" schedule: interval: "monthly" open-pull-requests-limit: 10 ignore: - dependency-name: "org.apache.qpid:qpid-jms-client" update-types: [ "version-update:semver-major" ] - package-ecosystem: "gradle" directory: "/modules/solr" schedule: interval: "monthly" open-pull-requests-limit: 10 ignore: - dependency-name: "org.apache.solr:solr-solrj" update-types: [ "version-update:semver-major" ] - package-ecosystem: "gradle" directory: "/modules/spock" schedule: interval: "monthly" ignore: - dependency-name: "org.junit:junit-bom" update-types: [ "version-update:semver-major" ] open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/tidb" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/timeplus" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/toxiproxy" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/trino" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/typesense" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/vault" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/weaviate" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/yugabytedb" schedule: interval: "monthly" open-pull-requests-limit: 10 # Examples - package-ecosystem: "gradle" directory: "/examples" schedule: interval: "monthly" open-pull-requests-limit: 10 ignore: - dependency-name: "ch.qos.logback:logback-classic" update-types: [ "version-update:semver-minor" ] - dependency-name: "org.apache.solr:solr-solrj" update-types: [ "version-update:semver-major" ] - dependency-name: "org.neo4j.driver:neo4j-java-driver" update-types: [ "version-update:semver-major" ] - dependency-name: "org.testng:testng" update-types: [ "version-update:semver-minor" ] - dependency-name: "org.slf4j:slf4j-api" update-types: [ "version-update:semver-major" ] - dependency-name: "org.springframework.boot" update-types: [ "version-update:semver-major" ] - dependency-name: "com.diffplug.spotless" update-types: [ "version-update:semver-major", "version-update:semver-minor" ] - dependency-name: "com.hazelcast:hazelcast" update-types: [ "version-update:semver-minor" ] - dependency-name: "org.junit.jupiter:junit-jupiter" update-types: [ "version-update:semver-major" ] - dependency-name: "org.junit.platform:junit-platform-launcher" update-types: [ "version-update:semver-major" ] - dependency-name: "com.gradleup.shadow" update-types: [ "version-update:semver-major" ] # Smoke test - package-ecosystem: "gradle" directory: "/smoke-test" schedule: interval: "monthly" open-pull-requests-limit: 10 ignore: - dependency-name: "ch.qos.logback:logback-classic" update-types: [ "version-update:semver-minor" ] - dependency-name: "com.diffplug.spotless" update-types: [ "version-update:semver-major", "version-update:semver-minor" ] - dependency-name: "org.junit.jupiter:junit-jupiter" update-types: [ "version-update:semver-major" ] - dependency-name: "org.junit.platform:junit-platform-launcher" update-types: [ "version-update:semver-major" ] - dependency-name: "com.gradleup.shadow" update-types: [ "version-update:semver-major" ] # GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" open-pull-requests-limit: 10 ================================================ FILE: .github/labeler.yml ================================================ "area/docker-compose": - changed-files: - any-glob-to-any-file: - core/src/main/java/org/testcontainers/containers/ComposeContainer.java - core/src/main/java/org/testcontainers/containers/ComposeDelegate.java - core/src/main/java/org/testcontainers/containers/DockerComposeContainer.java - core/src/main/java/org/testcontainers/containers/DockerComposeFiles.java - core/src/test/java/org/testcontainers/containers/Compose*Test.java - core/src/test/java/org/testcontainers/containers/DockerCompose*Test.java - core/src/test/java/org/testcontainers/junit/Compose*Test.java - core/src/test/java/org/testcontainers/junit/DockerCompose*Test.java "github_actions": - changed-files: - any-glob-to-any-file: - .github/workflows/* "gradle-wrapper": - changed-files: - any-glob-to-any-file: - gradle/wrapper/* - gradlew - gradlew.bat "modules/activemq": - changed-files: - any-glob-to-any-file: - modules/activemq/**/* "modules/azure": - changed-files: - any-glob-to-any-file: - modules/azure/**/* "modules/cassandra": - changed-files: - any-glob-to-any-file: - modules/cassandra/**/* "modules/chromadb": - changed-files: - any-glob-to-any-file: - modules/chromadb/**/* "modules/clickhouse": - changed-files: - any-glob-to-any-file: - modules/clickhouse/**/* "modules/cockroachdb": - changed-files: - any-glob-to-any-file: - modules/cockroachdb/**/* "modules/consul": - changed-files: - any-glob-to-any-file: - modules/consul/**/* "modules/couchbase": - changed-files: - any-glob-to-any-file: - modules/couchbase/**/* "modules/cratedb": - changed-files: - any-glob-to-any-file: - modules/cratedb/**/* "modules/databend": - changed-files: - any-glob-to-any-file: - modules/databend/**/* "modules/db2": - changed-files: - any-glob-to-any-file: - modules/db2/**/* "modules/dynalite": - changed-files: - any-glob-to-any-file: - modules/dynalite/**/* "modules/elasticsearch": - changed-files: - any-glob-to-any-file: - modules/elasticsearch/**/* "modules/gcloud": - changed-files: - any-glob-to-any-file: - modules/gcloud/**/* "modules/grafana": - changed-files: - any-glob-to-any-file: - modules/grafana/**/* "modules/hivemq": - changed-files: - any-glob-to-any-file: - modules/hivemq/**/* "modules/influx": - changed-files: - any-glob-to-any-file: - modules/influxdb/**/* "modules/jdbc": - changed-files: - any-glob-to-any-file: - modules/jdbc/**/* "modules/jupiter": - changed-files: - any-glob-to-any-file: - modules/junit-jupiter/**/* "modules/k3s": - changed-files: - any-glob-to-any-file: - modules/k3s/**/* "modules/k6": - changed-files: - any-glob-to-any-file: - modules/k6/**/* "modules/kafka": - changed-files: - any-glob-to-any-file: - modules/kafka/**/* "modules/ldap": - changed-files: - any-glob-to-any-file: - modules/ldap/**/* "modules/localstack": - changed-files: - any-glob-to-any-file: - modules/localstack/**/* "modules/mariadb": - changed-files: - any-glob-to-any-file: - modules/mariadb/**/* "modules/milvus": - changed-files: - any-glob-to-any-file: - modules/milvus/**/* "modules/minio": - changed-files: - any-glob-to-any-file: - modules/minio/**/* "modules/mockserver": - changed-files: - any-glob-to-any-file: - modules/mockserver/**/* "modules/mongodb": - changed-files: - any-glob-to-any-file: - modules/mongodb/**/* "modules/sql-server": - changed-files: - any-glob-to-any-file: - modules/mssqlserver/**/* "modules/mysql": - changed-files: - any-glob-to-any-file: - modules/mysql/**/* "modules/neo4j": - changed-files: - any-glob-to-any-file: - modules/neo4j/**/* "modules/nginx": - changed-files: - any-glob-to-any-file: - modules/nginx/**/* "modules/oceanbase": - changed-files: - any-glob-to-any-file: - modules/oceanbase/**/* "modules/ollama": - changed-files: - any-glob-to-any-file: - modules/ollama/**/* "modules/openfga": - changed-files: - any-glob-to-any-file: - modules/openfga/**/* "modules/oracle": - changed-files: - any-glob-to-any-file: - modules/oracle-free/**/* - modules/oracle-xe/**/* "modules/orientdb": - changed-files: - any-glob-to-any-file: - modules/orientdb/**/* "modules/pinecone": - changed-files: - any-glob-to-any-file: - modules/pinecone/**/* "modules/postgres": - changed-files: - any-glob-to-any-file: - modules/postgresql/**/* "modules/presto": - changed-files: - any-glob-to-any-file: - modules/presto/**/* "modules/pulsar": - changed-files: - any-glob-to-any-file: - modules/pulsar/**/* "modules/qdrant": - changed-files: - any-glob-to-any-file: - modules/qdrant/**/* "modules/questdb": - changed-files: - any-glob-to-any-file: - modules/questdb/**/* "modules/r2dbc": - changed-files: - any-glob-to-any-file: - modules/r2dbc/**/* "modules/rabbitmq": - changed-files: - any-glob-to-any-file: - modules/rabbitmq/**/* "modules/redpanda": - changed-files: - any-glob-to-any-file: - modules/redpanda/**/* "modules/scylladb": - changed-files: - any-glob-to-any-file: - modules/scylladb/**/* "modules/selenium": - changed-files: - any-glob-to-any-file: - modules/selenium/**/* "modules/solace": - changed-files: - any-glob-to-any-file: - modules/solace/**/* "modules/solr": - changed-files: - any-glob-to-any-file: - modules/solr/**/* "modules/spock": - changed-files: - any-glob-to-any-file: - modules/spock/**/* "modules/tidb": - changed-files: - any-glob-to-any-file: - modules/tidb/**/* "modules/timeplus": - changed-files: - any-glob-to-any-file: - modules/timeplus/**/* "modules/toxiproxy": - changed-files: - any-glob-to-any-file: - modules/toxiproxy/**/* "modules/trino": - 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: - modules/vault/**/* "modules/weaviate": - changed-files: - any-glob-to-any-file: - modules/weaviate/**/* "modules/yugabytedb": - changed-files: - any-glob-to-any-file: - modules/yugabytedb/**/* "type/docs": - changed-files: - any-glob-to-any-file: - docs/**/*.md ================================================ FILE: .github/pull_request_template.md ================================================ ================================================ FILE: .github/release-drafter.yml ================================================ name-template: $NEXT_PATCH_VERSION tag-template: $NEXT_PATCH_VERSION template: | # What's Changed $CHANGES categories: - title: ⚠️ Breaking API changes label: type/breaking-api-change - title: 🚀 Features & Enhancements labels: - type/feature - type/enhancement - title: ☠️ Deprecations label: type/deprecation - title: 🐛 Bug Fixes label: type/bug - title: 📖 Documentation label: type/docs - title: 🧹 Housekeeping labels: - type/housekeeping - type/test-improvement - title: 📦 Dependency updates label: dependencies collapse-after: 5 ================================================ FILE: .github/settings.yml ================================================ # These settings are synced to GitHub by https://probot.github.io/apps/settings/ repository: # See https://docs.github.com/en/rest/reference/repos#update-a-repository for all available settings. # The name of the repository. Changing this will rename the repository name: testcontainers-java # A short description of the repository that will show up on GitHub description: Testcontainers is a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container. # A URL with more information about the repository homepage: https://testcontainers.org # A comma-separated list of topics to set on the repository topics: java,testing,docker,docker-compose,jvm,test-automation,junit,hacktoberfest,integration-testing # Either `true` to make the repository private, or `false` to make it public. private: false # Either `true` to enable issues for this repository, `false` to disable them. has_issues: true # Either `true` to enable projects for this repository, or `false` to disable them. # If projects are disabled for the organization, passing `true` will cause an API error. has_projects: false # Either `true` to enable the wiki for this repository, `false` to disable it. has_wiki: false # Either `true` to enable downloads for this repository, `false` to disable them. has_downloads: false # Updates the default branch for this repository. default_branch: main # Either `true` to allow squash-merging pull requests, or `false` to prevent # squash-merging. allow_squash_merge: true # Either `true` to allow merging pull requests with a merge commit, or `false` # to prevent merging pull requests with merge commits. allow_merge_commit: false # Either `true` to allow rebase-merging pull requests, or `false` to prevent # rebase-merging. allow_rebase_merge: false # Either `true` to enable automatic deletion of branches on merge, or `false` to disable delete_branch_on_merge: true # Labels: define labels for Issues and Pull Requests # If including a `#`, make sure to wrap it with quotes! labels: - name: area/bitbucket-pipelines color: '#f9d0c4' - name: area/docker-compose color: '#f9d0c4' - name: area/logging color: '#f9d0c4' - name: area/shading color: '#f9d0c4' - name: area/test frameworks color: '#f9d0c4' - name: blocker color: '#b60205' - name: client/docker-for-mac color: '#c2e0c6' - name: client/docker-for-windows color: '#c2e0c6' - name: client/docker-machine color: '#c2e0c6' - name: client/in-container color: '#c2e0c6' - name: client/podman color: '#c2e0c6' - name: dependencies color: '#0025ff' - name: github_actions color: '#000000' - name: good first issue color: '#14d60a' - name: gradle-wrapper color: '#02303A' - name: hacktoberfest color: '#14d60a' - name: hacktoberfest-accepted color: '#79C259' - name: help wanted color: '#fef2c0' - name: modules/activemq color: '#006b75' - name: modules/azure color: '#006b75' - name: modules/cassandra color: '#006b75' - name: modules/chromadb color: '#006b75' - name: modules/clickhouse color: '#006b75' - name: modules/cockroachdb color: '#006b75' - name: modules/consul color: '#006b75' - name: modules/couchbase color: '#006b75' - name: modules/cratedb color: '#006b75' - name: modules/db2 color: '#006b75' - name: modules/dynalite color: '#006b75' - name: modules/elasticsearch color: '#006b75' - name: modules/gcloud color: '#006b75' - name: modules/grafana color: '#006b75' - name: modules/hivemq color: '#006b75' - name: modules/influx color: '#006b75' - name: modules/jdbc color: '#006b75' - name: modules/jupiter color: '#006b75' - name: modules/k3s color: '#006b75' - name: modules/k6 color: '#006b75' - name: modules/kafka color: '#006b75' - name: modules/ldap color: '#006b75' - name: modules/localstack color: '#006b75' - name: modules/mariadb color: '#006b75' - name: modules/milvus color: '#006b75' - name: modules/minio color: '#006b75' - name: modules/mockserver color: '#006b75' - name: modules/mongodb color: '#006b75' - name: modules/mysql color: '#006b75' - name: modules/neo4j color: '#006b75' - name: modules/nginx color: '#006b75' - name: modules/oceanbase color: '#006b75' - name: modules/ollama color: '#006b75' - name: modules/openfga color: '#006b75' - name: modules/oracle color: '#006b75' - name: modules/orientdb color: '#006b75' - name: modules/pinecone color: '#006b75' - name: modules/postgres color: '#006b75' - name: modules/presto color: '#006b75' - name: modules/pulsar color: '#006b75' - name: modules/qdrant color: '#006b75' - name: modules/questdb color: '#006b75' - name: modules/r2dbc color: '#006b75' - name: modules/rabbitmq color: '#006b75' - name: modules/redpanda color: '#006b75' - name: modules/selenium color: '#006b75' - name: modules/solace color: '#006b75' - name: modules/solr color: '#006b75' - name: modules/spock color: '#006b75' - name: modules/sql-server color: '#006b75' - name: modules/tidb color: '#006b75' - name: modules/timeplus color: '#006b75' - name: modules/toxiproxy color: '#006b75' - name: modules/trino color: '#006b75' - name: modules/typesense color: '#006b75' - name: modules/vault color: '#006b75' - name: modules/weaviate color: '#006b75' - name: modules/yugabytedb color: '#006b75' - name: modules/databend color: '#006b75' - name: os/linux color: '#1d76db' - name: os/macOS color: '#1d76db' - name: os/windows color: '#1d76db' - name: resolution/acknowledged color: '#fef2c0' - name: resolution/answered color: '#fef2c0' - name: resolution/awaiting-release color: '#fef2c0' - name: resolution/duplicate color: '#fef2c0' - name: resolution/invalid color: '#fef2c0' - name: resolution/pr-submitted color: '#fef2c0' - name: resolution/somedaymaybe color: '#fef2c0' - name: resolution/waiting-for-info color: '#fef2c0' - name: resolution/wontfix color: '#fef2c0' - name: security color: '#ee0701' - name: stale color: '#ffffff' - name: type/breaking-api-change color: '#d4c5f9' - name: type/bug color: '#d4c5f9' - name: type/deprecation color: '#d4c5f9' - name: type/docs color: '#d4c5f9' - name: type/enhancement color: '#d4c5f9' - name: type/feature color: '#d4c5f9' - name: type/housekeeping color: '#d4c5f9' - name: type/new module color: '#d4c5f9' - name: type/question color: '#d4c5f9' - name: type/test-improvement color: '#d4c5f9' # Collaborators: give specific users access to this repository. # See https://docs.github.com/en/rest/reference/repos#add-a-repository-collaborator for available options # collaborators: # - username: # permission: maintain # Note: `permission` is only valid on organization-owned repositories. # The permission to grant the collaborator. Can be one of: # * `pull` - can pull, but not push to or administer this repository. # * `push` - can pull and push, but not administer this repository. # * `admin` - can pull, push and administer this repository. # * `maintain` - Recommended for project managers who need to manage the repository without access to sensitive or destructive actions. # * `triage` - Recommended for contributors who need to proactively manage issues and pull requests without write access. # See https://docs.github.com/en/rest/reference/teams#add-or-update-team-repository-permissions for available options teams: # Please make sure the team already exist in the organization, as the repository-settings application is not creating them. # See https://github.com/repository-settings/app/discussions/639 for more information about teams and settings - name: java-team # The permission to grant the team. Can be one of: # * `pull` - can pull, but not push to or administer this repository. # * `push` - can pull and push, but not administer this repository. # * `admin` - can pull, push and administer this repository. # * `maintain` - Recommended for project managers who need to manage the repository without access to sensitive or destructive actions. # * `triage` - Recommended for contributors who need to proactively manage issues and pull requests without write access. permission: admin - name: oss-team permission: maintain branches: - name: main # https://docs.github.com/en/rest/reference/repos#update-branch-protection # Branch Protection settings. Set to null to disable protection: # Required. Require at least one approving review on a pull request, before merging. Set to null to disable. required_pull_request_reviews: # The number of approvals required. (1-6) required_approving_review_count: 1 # Dismiss approved reviews automatically when a new commit is pushed. dismiss_stale_reviews: true # Blocks merge until code owners have reviewed. require_code_owner_reviews: true # Specify which users and teams can dismiss pull request reviews. Pass an empty dismissal_restrictions object to disable. User and team dismissal_restrictions are only available for organization-owned repositories. Omit this parameter for personal repositories. dismissal_restrictions: users: [] teams: [java-team] # Required. Require status checks to pass before merging. Set to null to disable required_status_checks: # Required. Require branches to be up to date before merging. strict: true # Required. The list of status checks to require in order to merge into this branch contexts: ["core (17)", "core (21)", "check_docs_examples (:docs:examples:check)", "in-docker_test", "ci/circleci: minimal_core", "test"] # Required. Enforce all configured restrictions for administrators. Set to true to enforce required status checks for repository administrators. Set to null to disable. enforce_admins: false # Prevent merge commits from being pushed to matching branches required_linear_history: true # Required. Restrict who can push to this branch. Team and user restrictions are only available for organization-owned repositories. Set to null to disable. restrictions: apps: [] users: [] teams: [java-team] ================================================ FILE: .github/workflows/ci-docker-wormhole.yml ================================================ name: CI-Docker-Wormhole on: pull_request: paths-ignore: - '.github/ISSUE_TEMPLATE/*.yaml' - '.github/CODEOWNERS' - '.github/pull_request_template.md' - 'docs/**/*.css' - 'docs/**/*.html' - 'docs/**/*.ico' - 'docs/**/*.md' - 'docs/**/*.png' - 'docs/**/*.svg' - 'mkdocs.yml' - 'README.md' - 'RELEASING.md' - '.sdkmanrc' push: branches: [ main ] paths-ignore: - '.github/ISSUE_TEMPLATE/*.yaml' - '.github/CODEOWNERS' - '.github/pull_request_template.md' - 'docs/**/*.css' - 'docs/**/*.html' - 'docs/**/*.ico' - 'docs/**/*.md' - 'docs/**/*.png' - 'docs/**/*.svg' - 'mkdocs.yml' - 'README.md' - 'RELEASING.md' - '.sdkmanrc' concurrency: group: "${{ github.workflow }}-${{ github.head_ref || github.sha }}" cancel-in-progress: true permissions: contents: read jobs: in-docker_test: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v5 - name: Build with Gradle run: | docker run -i --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ -v "$HOME:$HOME" \ -v "$PWD:$PWD" \ -w "$PWD" \ -e AUTO_APPLY_GIT_HOOKS=false \ eclipse-temurin:17-jdk-alpine \ ./gradlew --no-daemon --continue --scan testcontainers:test --tests '*GenericContainerRuleTest' ================================================ FILE: .github/workflows/ci-rootless.yml ================================================ name: CI-Docker-Rootless on: pull_request: paths-ignore: - '.github/ISSUE_TEMPLATE/*.yaml' - '.github/CODEOWNERS' - '.github/pull_request_template.md' - 'docs/**/*.css' - 'docs/**/*.html' - 'docs/**/*.ico' - 'docs/**/*.md' - 'docs/**/*.png' - 'docs/**/*.svg' - 'mkdocs.yml' - 'README.md' - 'RELEASING.md' - '.sdkmanrc' push: branches: [ main ] paths-ignore: - '.github/ISSUE_TEMPLATE/*.yaml' - '.github/CODEOWNERS' - '.github/pull_request_template.md' - 'docs/**/*.css' - 'docs/**/*.html' - 'docs/**/*.ico' - 'docs/**/*.md' - 'docs/**/*.png' - 'docs/**/*.svg' - 'mkdocs.yml' - 'README.md' - 'RELEASING.md' - '.sdkmanrc' concurrency: group: "${{ github.workflow }}-${{ github.head_ref || github.sha }}" cancel-in-progress: true permissions: contents: read jobs: test: runs-on: ubuntu-22.04 permissions: checks: write steps: - uses: actions/checkout@v5 - name: Setup rootless Docker uses: docker/setup-docker-action@v4 with: rootless: true - name: Setup Gradle Build Action uses: gradle/actions/setup-gradle@v5 - name: Build with Gradle run: ./gradlew --no-daemon --scan testcontainers:test --tests '*GenericContainerRuleTest' - uses: ./.github/actions/setup-junit-report ================================================ FILE: .github/workflows/ci-windows-trigger.yml ================================================ name: windows-test command dispatch on: issue_comment: types: [created] permissions: contents: read jobs: windows-test-command-trigger: permissions: pull-requests: write # for peter-evans/slash-command-dispatch to create PR reaction runs-on: ubuntu-latest steps: - name: Trigger windows-test command uses: peter-evans/slash-command-dispatch@13bc09769d122a64f75aa5037256f6f2d78be8c4 # v4.0.0 with: token: ${{ secrets.WINDOWS_WORKERS_TOKEN }} # The command to trigger the pipeline: e.g. /windows-test # The command name must match the name of the repository_dispatch.type in 'ci-windows.yml' workflow, using '-command' as suffix. E.g. 'windows-test-command' commands: windows-test issue-type: pull-request # The user that owns the above token must belong to the elevated role of 'Maintainers' permission: maintain reactions: false ================================================ FILE: .github/workflows/ci-windows.yml ================================================ name: CI - Windows on: pull_request: paths-ignore: - '.github/ISSUE_TEMPLATE/*.yaml' - '.github/CODEOWNERS' - '.github/pull_request_template.md' - 'docs/**/*.css' - 'docs/**/*.html' - 'docs/**/*.ico' - 'docs/**/*.md' - 'docs/**/*.png' - 'docs/**/*.svg' - 'mkdocs.yml' - 'README.md' - 'RELEASING.md' - '.sdkmanrc' push: branches: [ main ] paths-ignore: - '.github/ISSUE_TEMPLATE/*.yaml' - '.github/CODEOWNERS' - '.github/pull_request_template.md' - 'docs/**/*.css' - 'docs/**/*.html' - 'docs/**/*.ico' - 'docs/**/*.md' - 'docs/**/*.png' - 'docs/**/*.svg' - 'mkdocs.yml' - 'README.md' - 'RELEASING.md' - '.sdkmanrc' repository_dispatch: types: [windows-test-command] concurrency: group: "${{ github.workflow }}-${{ github.head_ref || github.sha }}" cancel-in-progress: true permissions: contents: read env: DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} jobs: main: if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} runs-on: self-hosted permissions: checks: write steps: - uses: actions/checkout@v5 - uses: ./.github/actions/setup-build - name: Build with Gradle run: ./gradlew.bat cleanTest testcontainers:test --no-daemon --continue --scan --no-build-cache - uses: ./.github/actions/setup-junit-report pr: if: ${{ github.event.client_payload.slash_command.command == 'windows-test' }} runs-on: self-hosted permissions: checks: write statuses: write steps: - name: Create pending status uses: actions/github-script@v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | github.rest.repos.createCommitStatus({ owner: context.repo.owner, repo: context.repo.repo, sha: context.payload.client_payload.pull_request.head.sha, state: 'pending', target_url: `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`, context: 'CI - Windows', }) - uses: actions/checkout@v5 with: token: ${{ secrets.GITHUB_TOKEN }} repository: ${{ github.event.client_payload.pull_request.head.repo.full_name }} ref: ${{ github.event.client_payload.pull_request.head.ref }} - uses: ./.github/actions/setup-build - name: Build with Gradle run: ./gradlew.bat cleanTest testcontainers:test --no-daemon --continue --scan --no-build-cache - uses: ./.github/actions/setup-junit-report - name: Create success status uses: actions/github-script@v8 if: success() with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | github.rest.repos.createCommitStatus({ owner: context.repo.owner, repo: context.repo.repo, sha: context.payload.client_payload.pull_request.head.sha, state: 'success', target_url: `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`, context: 'CI - Windows', }) - name: Create failure status uses: actions/github-script@v8 if: failure() with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | github.rest.repos.createCommitStatus({ owner: context.repo.owner, repo: context.repo.repo, sha: context.payload.client_payload.pull_request.head.sha, state: 'failure', target_url: `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`, context: 'CI - Windows', }) ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: pull_request: paths-ignore: - '.github/ISSUE_TEMPLATE/*.yaml' - '.github/CODEOWNERS' - '.github/pull_request_template.md' - 'docs/**/*.css' - 'docs/**/*.html' - 'docs/**/*.ico' - 'docs/**/*.md' - 'docs/**/*.png' - 'docs/**/*.svg' - 'mkdocs.yml' - 'README.md' - 'RELEASING.md' - '.sdkmanrc' push: branches: [ main ] paths-ignore: - '.github/ISSUE_TEMPLATE/*.yaml' - '.github/CODEOWNERS' - '.github/pull_request_template.md' - 'docs/**/*.css' - 'docs/**/*.html' - 'docs/**/*.ico' - 'docs/**/*.md' - 'docs/**/*.png' - 'docs/**/*.svg' - 'mkdocs.yml' - 'README.md' - 'RELEASING.md' - '.sdkmanrc' concurrency: group: "${{ github.workflow }}-${{ github.head_ref || github.sha }}" cancel-in-progress: true permissions: contents: read env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} jobs: core: runs-on: ubuntu-22.04 permissions: checks: write strategy: matrix: java: [ '17', '21' ] steps: - uses: actions/checkout@v5 - uses: ./.github/actions/setup-build with: java-version: ${{ matrix.java }} - name: Build and test with Gradle run: | ./gradlew :testcontainers:check --no-daemon --continue --scan - uses: ./.github/actions/setup-junit-report turbo-mode: if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} runs-on: ubuntu-22.04 permissions: checks: write steps: - uses: actions/checkout@v5 - uses: ./.github/actions/setup-build - name: Setup Testcontainers Cloud Client uses: atomicjar/testcontainers-cloud-setup-action@main with: token: ${{ secrets.TC_CLOUD_TOKEN }} args: --max-concurrency=4 - name: Test using Testcontainers Cloud with Turbo Mode enabled working-directory: ./smoke-test/ run: | ./gradlew check --no-daemon --continue --scan --info - uses: ./.github/actions/setup-junit-report find_gradle_jobs: needs: [core] runs-on: ubuntu-22.04 outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - uses: actions/checkout@v5 - uses: ./.github/actions/setup-java - name: Setup Gradle Build Action uses: gradle/actions/setup-gradle@v5 - id: set-matrix env: # Since we override the tests executor, # we should not push empty results to the cache READ_ONLY_REMOTE_GRADLE_CACHE: true run: | TASKS=$(./gradlew --no-daemon --parallel -q testMatrix | jq 'del(.[] | select(. == ":testcontainers:check" or startswith(":docs:")))' --compact-output) echo $TASKS echo "matrix={\"gradle_args\":$TASKS}" >> $GITHUB_OUTPUT check: needs: [find_gradle_jobs] strategy: fail-fast: false matrix: ${{ fromJson(needs.find_gradle_jobs.outputs.matrix) }} runs-on: ubuntu-22.04 permissions: checks: write steps: - uses: actions/checkout@v5 - uses: ./.github/actions/setup-build - name: Build and test with Gradle (${{matrix.gradle_args}}) run: | ./gradlew --no-daemon --continue --scan ${{matrix.gradle_args}} - uses: ./.github/actions/setup-junit-report find_examples_jobs: needs: [check] runs-on: ubuntu-22.04 outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - uses: actions/checkout@v5 - uses: ./.github/actions/setup-java - name: Setup Gradle Build Action uses: gradle/actions/setup-gradle@v5 - id: set-matrix working-directory: ./examples/ env: # Since we override the tests executor, # we should not push empty results to the cache READ_ONLY_REMOTE_GRADLE_CACHE: true run: | TASKS=$(./gradlew --no-daemon --parallel -q testMatrix) echo $TASKS echo "matrix={\"gradle_args\":$TASKS}" >> $GITHUB_OUTPUT check_examples: needs: [find_examples_jobs] strategy: fail-fast: false matrix: ${{ fromJson(needs.find_examples_jobs.outputs.matrix) }} runs-on: ubuntu-22.04 permissions: checks: write steps: - uses: actions/checkout@v5 - uses: ./.github/actions/setup-build - name: Build and test Examples with Gradle (${{matrix.gradle_args}}) working-directory: ./examples/ run: | ./gradlew --no-daemon --continue --scan --info ${{matrix.gradle_args}} - uses: ./.github/actions/setup-junit-report find_docs_examples_jobs: needs: [check_examples] runs-on: ubuntu-22.04 outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - uses: actions/checkout@v5 - uses: ./.github/actions/setup-java - name: Setup Gradle Build Action uses: gradle/actions/setup-gradle@v5 - id: set-matrix env: # Since we override the tests executor, # we should not push empty results to the cache READ_ONLY_REMOTE_GRADLE_CACHE: true run: | TASKS=$(./gradlew --no-daemon --parallel -q testMatrix | jq 'map(select(startswith(":docs:")))' --compact-output) echo $TASKS echo "matrix={\"gradle_args\":$TASKS}" >> $GITHUB_OUTPUT check_docs_examples: needs: [find_docs_examples_jobs] strategy: fail-fast: false matrix: ${{ fromJson(needs.find_docs_examples_jobs.outputs.matrix) }} runs-on: ubuntu-22.04 permissions: checks: write steps: - uses: actions/checkout@v5 - uses: ./.github/actions/setup-build - name: Build and test with Gradle (${{matrix.gradle_args}}) run: | ./gradlew --no-daemon --continue --scan ${{matrix.gradle_args}} - uses: ./.github/actions/setup-junit-report ================================================ FILE: .github/workflows/combine-prs.yml ================================================ name: Combine PRs on: workflow_dispatch: jobs: combine-prs: permissions: contents: write pull-requests: write checks: read runs-on: ubuntu-latest steps: - name: combine-prs id: combine-prs uses: github/combine-prs@v5.2.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/labeler.yml ================================================ name: "Pull Request Labeler" on: - pull_request_target jobs: triage: permissions: contents: read pull-requests: write runs-on: ubuntu-latest steps: - uses: actions/labeler@v6 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" ================================================ FILE: .github/workflows/moby-latest.yml ================================================ name: Tests against recent Docker engine releases on: workflow_dispatch: inputs: version: description: 'Docker version' required: false default: 'latest' type: string schedule: # nightly build, at 23:59 CEST - cron: '59 23 * * *' env: DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} jobs: test_docker: strategy: matrix: include: - { install-docker-type: "STABLE", version: "${{ inputs.version }}", channel: stable, rootless: false } - { install-docker-type: "ROOTLESS", version: "${{ inputs.version }}", channel: stable, rootless: true } - { install-docker-type: "ROOTFUL", version: edge, channel: test, rootless: false } name: "Core tests using Docker ${{ matrix.install-docker-type }} (channel ${{ matrix.channel }})" runs-on: ubuntu-22.04 continue-on-error: true steps: - uses: actions/checkout@v5 - uses: ./.github/actions/setup-build - name: Install Stable Docker id: setup_docker uses: docker/setup-docker-action@v4 with: version: ${{ matrix.version }} channel: ${{ matrix.channel }} rootless: ${{ matrix.rootless }} - name: Check Docker version run: docker version - name: Build with Gradle run: ./gradlew cleanTest --no-daemon --continue --scan -Dscan.tag.DOCKER_${{ matrix.install-docker-type }} testcontainers:test -Dorg.gradle.caching=false env: DOCKER_HOST: ${{steps.setup_docker.outputs.sock}} - uses: ./.github/actions/setup-junit-report - name: Notify to Slack on failures if: failure() id: slack uses: slackapi/slack-github-action@v2.1.1 with: payload: | { "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 }}/attempts/${{ github.run_attempt }}", "tc_github_action_status": "FAILED", "tc_slack_channel_id": "${{ secrets.SLACK_DOCKER_LATEST_CHANNEL_ID }}" } env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_DOCKER_LATEST_WEBHOOK }} ================================================ FILE: .github/workflows/release-drafter.yml ================================================ name: Release Drafter on: push: branches: - main pull_request: types: [opened, reopened, synchronize] permissions: contents: read jobs: update_release_draft: permissions: contents: write # for release-drafter/release-drafter to create a github release pull-requests: write # for release-drafter/release-drafter to add label to PR if: github.repository == 'testcontainers/testcontainers-java' runs-on: ubuntu-latest steps: - uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v5.19.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: release: types: [published] env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} permissions: contents: read jobs: release: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v5 - uses: ./.github/actions/setup-java - name: Clear existing docker image cache run: docker image prune -af - name: Setup Gradle Build Action uses: gradle/actions/setup-gradle@v5 - name: Run Gradle Build run: ./gradlew build --scan --no-daemon -i -x test - name: Run Gradle Publish run: | ./gradlew publish \ -Pversion="${{github.event.release.tag_name}}" --scan --no-daemon -i - name: Run Gradle Deploy run: | ./gradlew jreleaserDeploy -Pversion="${{github.event.release.tag_name}}" --scan --no-daemon -i env: JRELEASER_GPG_PUBLIC_KEY: ${{ vars.GPG_PUBLIC_KEY }} JRELEASER_GPG_SECRET_KEY: ${{ secrets.GPG_SIGNING_KEY }} JRELEASER_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.JRELEASER_MAVENCENTRAL_USERNAME }} JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.JRELEASER_MAVENCENTRAL_PASSWORD }} ================================================ FILE: .github/workflows/scripts/check_ci_status.sh ================================================ #!/bin/bash set -o errexit set -o nounset set -o pipefail set -o xtrace if [ -z $1 ] ; then echo "First parameter (commit SHA) is required!" && exit 1; fi STATUS=$(curl -s https://api.github.com/repos/testcontainers/testcontainers-java/commits/$1/status | jq -r '.state') [ "$STATUS" == 'success' ] ================================================ FILE: .github/workflows/update-docs-version.yml ================================================ name: Update docs version on: release: types: [ published ] permissions: contents: read jobs: build: permissions: contents: write # for peter-evans/create-pull-request to create branch pull-requests: write # for peter-evans/create-pull-request to create a PR if: github.repository == 'testcontainers/testcontainers-java' runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v5 with: ref: main - name: Update latest_version property in mkdocs.yml run: | sed -i "s/latest_version: .*/latest_version: ${GITHUB_REF_NAME}/g" mkdocs.yml git diff - name: Create Pull Request uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v3.10.1 with: title: Update docs version to ${{ github.ref_name }} body: | Update docs version to ${{ github.ref_name }} skip-checks: true branch: update-docs-version delete-branch: true ================================================ FILE: .github/workflows/update-gradle-wrapper.yml ================================================ name: Update Gradle Wrapper on: schedule: - cron: "0 0 * * *" permissions: contents: read jobs: update-gradle-wrapper: permissions: contents: write # for gradle-update/update-gradle-wrapper-action pull-requests: write # for gradle-update/update-gradle-wrapper-action if: github.repository == 'testcontainers/testcontainers-java' runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Update Gradle Wrapper uses: gradle-update/update-gradle-wrapper-action@512b1875f3b6270828abfe77b247d5895a2da1e5 # v1.0.13 with: repo-token: ${{ secrets.GITHUB_TOKEN }} labels: dependencies - uses: gradle/actions/wrapper-validation@v5 ================================================ FILE: .github/workflows/update-testcontainers-version.yml ================================================ name: Update testcontainers version on: release: types: [ published ] permissions: contents: read jobs: build: permissions: contents: write # for peter-evans/create-pull-request to create branch pull-requests: write # for peter-evans/create-pull-request to create a PR if: github.repository == 'testcontainers/testcontainers-java' runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v5 with: ref: main - name: Update testcontainers.version property in gradle.properties run: | sed -i "s/^testcontainers\.version=.*/testcontainers\.version=${GITHUB_REF_NAME}/g" gradle.properties git diff - name: Create Pull Request uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v3.10.1 with: title: Update testcontainers version to ${{ github.ref_name }} body: | Update testcontainers version to ${{ github.ref_name }} branch: update-tc-version delete-branch: true ================================================ FILE: .gitignore ================================================ /.mvn/timing.properties # Created by .ignore support plugin (hsz.mobi) ### Maven template target/ dependency-reduced-pom.xml ### JetBrains template # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm *.iml ## Directory-based project format: .idea/* !.idea/icon.png # if you remove the above rule, at least ignore the following: # User-specific stuff: # .idea/workspace.xml # .idea/tasks.xml # .idea/dictionaries # Sensitive or high-churn files: # .idea/dataSources.ids # .idea/dataSources.xml # .idea/sqlDataSources.xml # .idea/dynamic.xml # .idea/uiDesigner.xml # Gradle: # .idea/gradle.xml # .idea/libraries # Mongo Explorer plugin: # .idea/mongoSettings.xml ## File-based project format: ## Plugin-specific files: # IntelliJ *.lastchange # Vagrant .vagrant/ # Gitbook _book/ node_modules/ .gradle/ build/ !buildSrc/src/main/groovy/org/testcontainers/build out/ *.class # Eclipse IDE files **/.project **/.classpath **/.settings **/bin/ **/out/ # Generated docs site/ .direnv/ src/mkdocs-codeinclude-plugin src/pip-delete-this-directory.txt .DS_Store # Codespaces / VSCode /.vscode/ # Python virtual environment /.venv/ ================================================ FILE: .sdkmanrc ================================================ # Enable auto-env through the sdkman_auto_env config # Add key=value pairs of SDKs to use below java=17.0.16-tem ================================================ FILE: AUTHORS ================================================ This file is deprecated. Please see the [contributors](https://github.com/testcontainers/testcontainers-java/graphs/contributors) listing. [Richard North](https://github.com/rnorth), [Sergei Egorov](https://github.com/bsideup) and [Kevin Wittek](https://github.com/kiview) are the core maintainers. ================================================ FILE: CHANGELOG.md ================================================ # Change Log ~All notable changes to this project will be documented in this file.~ # MOVED **After version 1.8.3 all future releases will _only_ be documented in the [Releases](https://github.com/testcontainers/testcontainers-java/releases) section of the GitHub repository. This changelog file will eventually be removed.** ## [1.8.3] - 2018-08-05 ### Fixed - Fixed `with*` methods of `CouchbaseContainer` ([\#810](https://github.com/testcontainers/testcontainers-java/pull/810)) - Fix problem with gzip encoded streams (e.g. copy file from container), by adding decompression support to netty exec factory (#817, fixes #681, relates to docker-java/docker-java#1079) ## [1.8.2] - 2018-07-31 ### Fixed - Add support for transparently using local images with docker-compose ([\#798](https://github.com/testcontainers/testcontainers-java/pull/798), fixes [\#674](https://github.com/testcontainers/testcontainers-java/issues/674)) - Fix bug with Dockerfile image creation with Docker for Mac 18.06-ce ([\#808](https://github.com/testcontainers/testcontainers-java/pull/808), fixes [\#680](https://github.com/testcontainers/testcontainers-java/issues/680)) ### Changed - Update Visible Assertions to 2.1.1 ([\#779](https://github.com/testcontainers/testcontainers-java/pull/779)). - KafkaContainer optimization (`group.initial.rebalance.delay.ms=0`) ([\#782](https://github.com/testcontainers/testcontainers-java/pull/782)). ## [1.8.1] - 2018-07-10 ### Fixed - Linux/Mac: Added support for docker credential helpers so that images may be pulled from private registries. See [\#729](https://github.com/testcontainers/testcontainers-java/issues/729), [\#647](https://github.com/testcontainers/testcontainers-java/issues/647) and [\#567](https://github.com/testcontainers/testcontainers-java/issues/567). - Ensure that the `COMPOSE_FILE` environment variable is populated with all relevant compose file names when running docker-compose in local mode [\#755](https://github.com/testcontainers/testcontainers-java/issues/755). - Fixed issue whereby specified command in MariaDB image was not being applied. ([\#534](https://github.com/testcontainers/testcontainers-java/issues/534)) - Changed Oracle thin URL to support both Oracle 11 and 12 XE ([\#769](https://github.com/testcontainers/testcontainers-java/issues/769)) - Ensure that full JDBC URL query string is passed to JdbcDatabaseDelegate during initscript invocation ([\#741](https://github.com/testcontainers/testcontainers-java/issues/741); fixes [\#727](https://github.com/testcontainers/testcontainers-java/issues/727)) - Ensure that necessary transitive dependency inclusions are applied to generated project POMs ([\#772](https://github.com/testcontainers/testcontainers-java/issues/772); fixes [\#753](https://github.com/testcontainers/testcontainers-java/issues/753) and [\#652](https://github.com/testcontainers/testcontainers-java/issues/652)) ### Changed - Update Apache Pulsar module to 2.0.1 [\#760](https://github.com/testcontainers/testcontainers-java/issues/760). - Make JdbcDatabaseContainer#getDriverClassName public [\#743](https://github.com/testcontainers/testcontainers-java/pull/743). - enable `copyFileToContainer` feature during container startup [\#742](https://github.com/testcontainers/testcontainers-java/pull/742). - avoid using file mounting in KafkaContainer [\#775](https://github.com/testcontainers/testcontainers-java/pull/775). - Added Apache Cassandra module [\#776](https://github.com/testcontainers/testcontainers-java/pull/776). ## [1.8.0] - 2018-06-14 ### Fixed - Fixed JDBC URL Regex Pattern to ensure all supported Database URL's are accepted ([\#596](https://github.com/testcontainers/testcontainers-java/issues/596)) - Filtered out TestContainer parameters (TC_*) from query string before passing to database ([\#345](https://github.com/testcontainers/testcontainers-java/issues/345)) - Use `latest` tag as default image tag ([\#676](https://github.com/testcontainers/testcontainers-java/issues/676)) ### Changed - Allow `HttpWaitStrategy` to wait for a specific port ([\#703](https://github.com/testcontainers/testcontainers-java/pull/703)) - New module: Apache Pulsar ([\#713](https://github.com/testcontainers/testcontainers-java/pull/713)) - Add support for defining container labels ([\#725](https://github.com/testcontainers/testcontainers-java/pull/725)) - Use `quay.io/testcontainers/ryuk` instead of `bsideup/ryuk` ([\#721](https://github.com/testcontainers/testcontainers-java/pull/721)) - Added Couchbase module ([\#688](https://github.com/testcontainers/testcontainers-java/pull/688)) - Enhancements and Fixes for JDBC URL usage to create Containers ([\#594](https://github.com/testcontainers/testcontainers-java/pull/594)) - Extracted JDBC URL manipulations to a separate class - `ConnectionUrl`. - Added an overloaded method `JdbcDatabaseContainerProvider.newInstance(ConnectionUrl)`, with default implementation delegating to the existing `newInstance(tag)` method. (Relates to [\#566](https://github.com/testcontainers/testcontainers-java/issues/566)) - Added an implementation of `MySQLContainerProvider.newInstance(ConnectionUrl)` that uses Database Name, User, and Password from JDBC URL while creating new MySQL Container. ([\#566](https://github.com/testcontainers/testcontainers-java/issues/566) for MySQL Container) - Changed **internal** port of KafkaContainer back to 9092 ([\#733](https://github.com/testcontainers/testcontainers-java/pull/733)) - Add support for Dockerfile based images to OracleContainer ([\#734](https://github.com/testcontainers/testcontainers-java/pull/734)) - Read from both `/proc/net/tcp` and `/proc/net/tcp6` in `InternalCommandPortListeningCheck` ([\#750](https://github.com/testcontainers/testcontainers-java/pull/750)) - Added builder methods for timeouts in `JdbcDatabaseContainer` ([\#748](https://github.com/testcontainers/testcontainers-java/pull/748)) - Added an alternative experimental transport based on OkHttp. Enable it with `transport.type=okhttp` property ([\#710](https://github.com/testcontainers/testcontainers-java/pull/710)) - Framework-agnostic container & test lifecycle ([\#702](https://github.com/testcontainers/testcontainers-java/pull/702)) ## [1.7.3] - 2018-05-16 ### Fixed - Fix for setting `ryuk.container.timeout` causes a `ClassCastException` ([\#684](https://github.com/testcontainers/testcontainers-java/issues/684)) - Fixed provided but shaded dependencies in modules ([\#693](https://github.com/testcontainers/testcontainers-java/issues/693)) ### Changed - Added InfluxDB module ([\#686](https://github.com/testcontainers/testcontainers-java/pull/686)) - Added MockServer module ([\#696](https://github.com/testcontainers/testcontainers-java/pull/696)) - Changed LocalStackContainer to extend GenericContainer ([\#695](https://github.com/testcontainers/testcontainers-java/pull/695)) ## [1.7.2] - 2018-04-30 - Add support for private repositories using docker credential stores/helpers (fixes [\#567](https://github.com/testcontainers/testcontainers-java/issues/567)) ### Fixed - Add support for private repositories using docker credential stores/helpers (fixes [\#567](https://github.com/testcontainers/testcontainers-java/issues/567)) - Retry any exceptions (not just `DockerClientException`) on image pull ([\#662](https://github.com/testcontainers/testcontainers-java/issues/662)) - Fixed handling of the paths with `+` in them ([\#664](https://github.com/testcontainers/testcontainers-java/issues/664)) ### Changed - Database container images are now pinned to a specific version rather than using `latest`. The tags selected are the most recent as of the time of this change. If a JDBC URL is used with no tag specified, a WARN level log message is output, pending a future change to make tags mandatory in the JDBC URL. ([\#671](https://github.com/testcontainers/testcontainers-java/issues/671)) - Updated docker-java to 3.1.0-rc-3, enforced `org.jetbrains:annotations:15.0`. ([\#672](https://github.com/testcontainers/testcontainers-java/issues/672)) ## [1.7.1] - 2018-04-20 ### Fixed - Fixed missing `commons-codec` dependency ([\#642](https://github.com/testcontainers/testcontainers-java/issues/642)) - Fixed `HostPortWaitStrategy` throws `NumberFormatException` when port is exposed but not mapped ([\#640](https://github.com/testcontainers/testcontainers-java/issues/640)) - Fixed log processing: multibyte unicode, linebreaks and ASCII color codes. Color codes can be turned on with `withRemoveAnsiCodes(false)` ([\#643](https://github.com/testcontainers/testcontainers-java/pull/643)) - Fixed Docker host IP detection within docker container (detect only if not explicitly set) ([\#648](https://github.com/testcontainers/testcontainers-java/pull/648)) - Add support for private repositories using docker credential stores/helpers ([PR \#647](https://github.com/testcontainers/testcontainers-java/pull/647), fixes [\#567](https://github.com/testcontainers/testcontainers-java/issues/567)) ### Changed - Support multiple HTTP status codes for HttpWaitStrategy ([\#630](https://github.com/testcontainers/testcontainers-java/issues/630)) - Mark all long-living threads started by Testcontainers as daemons and group them. ([\#646](https://github.com/testcontainers/testcontainers-java/issues/646)) - Remove noisy `DEBUG` logging of Netty packets ([\#646](https://github.com/testcontainers/testcontainers-java/issues/646)) - Updated docker-java to 3.1.0-rc-2 ([\#646](https://github.com/testcontainers/testcontainers-java/issues/646)) ## [1.7.0] - 2018-04-07 ### Fixed - Fixed extraneous insertion of `useSSL=false` in all JDBC URL strings, even for DBs that do not understand it. Usage is now restricted to MySQL by default and can be overridden by authors of `JdbcDatabaseContainer` subclasses ([\#568](https://github.com/testcontainers/testcontainers-java/issues/568)) - Fixed `getServicePort` on `DockerComposeContainer` throws NullPointerException if service instance number in not used. ([\#619](https://github.com/testcontainers/testcontainers-java/issues/619)) - Increase Ryuk's timeout and make it configurable with `ryuk.container.timeout`. ([\#621](https://github.com/testcontainers/testcontainers-java/issues/621)[\#635](https://github.com/testcontainers/testcontainers-java/issues/635)) ### Changed - Added compatibility with selenium greater than 3.X ([\#611](https://github.com/testcontainers/testcontainers-java/issues/611)) - Abstracted and changed database init script functionality to support use of SQL-like scripts with non-JDBC connections. ([\#551](https://github.com/testcontainers/testcontainers-java/pull/551)) - Added `JdbcDatabaseContainer(Future)` constructor. ([\#543](https://github.com/testcontainers/testcontainers-java/issues/543)) - Mark DockerMachineClientProviderStrategy as not persistable ([\#593](https://github.com/testcontainers/testcontainers-java/pull/593)) - Added `waitingFor(String serviceName, WaitStrategy waitStrategy)` and overloaded `withExposedService()` methods to `DockerComposeContainer` to allow user to define `WaitStrategy` for compose containers. ([\#174](https://github.com/testcontainers/testcontainers-java/issues/174), [\#515](https://github.com/testcontainers/testcontainers-java/issues/515) and ([\#600](https://github.com/testcontainers/testcontainers-java/pull/600))) - Deprecated `WaitStrategy` and implementations in favour of classes with same names in `org.testcontainers.containers.strategy` ([\#600](https://github.com/testcontainers/testcontainers-java/pull/600)) - Added `ContainerState` interface representing the state of a started container ([\#600](https://github.com/testcontainers/testcontainers-java/pull/600)) - Added `WaitStrategyTarget` interface which is the target of the new `WaitStrategy` ([\#600](https://github.com/testcontainers/testcontainers-java/pull/600)) - *Breaking:* Removed hard-coded `wnameless` Oracle database image name. Users should instead place a file on the classpath named `testcontainers.properties` containing `oracle.container.image=IMAGE`, where IMAGE is a suitable image name and tag/SHA hash. For information, the approach recommended by Oracle for creating an Oracle XE docker image is described [here](https://blogs.oracle.com/oraclewebcentersuite/implement-oracle-database-xe-as-docker-containers). - Added `DockerHealthcheckWaitStrategy` that is based on Docker's built-in [healthcheck](https://docs.docker.com/engine/reference/builder/#healthcheck) ([\#618](https://github.com/testcontainers/testcontainers-java/pull/618)). - Added `withLogConsumer(String serviceName, Consumer consumer)` method to `DockerComposeContainer` ([\#605](https://github.com/testcontainers/testcontainers-java/issues/605)) - Added `withFixedExposedPort(int hostPort, int containerPort, InternetProtocol protocol)` method to `FixedHostPortGenericContainer` and `addFixedExposedPort(int hostPort, int containerPort, InternetProtocol protocol)` to `GenericContainer` ([\#586](https://github.com/testcontainers/testcontainers-java/pull/586)) ## [1.6.0] - 2018-01-28 ### Fixed - Fixed incompatibility of Docker-Compose container with JDK9. ([\#562](https://github.com/testcontainers/testcontainers-java/pull/562)) - Fixed retrieval of Docker host IP when running inside Docker. ([\#479](https://github.com/testcontainers/testcontainers-java/issues/479)) - Compose is now able to pull images from private repositories. ([\#536](https://github.com/testcontainers/testcontainers-java/issues/536)) - Fixed overriding MySQL image command. ([\#534](https://github.com/testcontainers/testcontainers-java/issues/534)) - Fixed shading for javax.annotation.CheckForNull ([\#563](https://github.com/testcontainers/testcontainers-java/issues/563) and [testcontainers/testcontainers-scala\#11](https://github.com/testcontainers/testcontainers-scala/issues/11)). ### Changed - Added JDK9 build and tests to Travis-CI. ([\#562](https://github.com/testcontainers/testcontainers-java/pull/562)) - Added Kafka module ([\#546](https://github.com/testcontainers/testcontainers-java/pull/546)) - Added "Death Note" to track & kill spawned containers even if the JVM was "kill -9"ed ([\#545](https://github.com/testcontainers/testcontainers-java/pull/545)) - Environment variables are now stored as Map instead of List ([\#550](https://github.com/testcontainers/testcontainers-java/pull/550)) - Added `withEnv(String name, Function, String> mapper)` with optional previous value ([\#550](https://github.com/testcontainers/testcontainers-java/pull/550)) - Added `withFileSystemBind` overloaded method with `READ_WRITE` file mode by default ([\#550](https://github.com/testcontainers/testcontainers-java/pull/550)) - All connections to JDBC containers (e.g. MySQL) don't use SSL anymore. ([\#374](https://github.com/testcontainers/testcontainers-java/issues/374)) ## [1.5.1] - 2017-12-19 ### Fixed - Fixed problem with case-sensitivity when checking internal port. ([\#524](https://github.com/testcontainers/testcontainers-java/pull/524)) - Add retry logic around checkExposedPort pre-flight check for improved robustness ([\#513](https://github.com/testcontainers/testcontainers-java/issues/513)) ### Changed - Added `getDatabaseName` method to JdbcDatabaseContainer, MySQLContainer, PostgreSQLContainer ([\#473](https://github.com/testcontainers/testcontainers-java/issues/473)) - Added `VncRecordingContainer` - Network-based, attachable re-implementation of `VncRecordingSidekickContainer` ([\#526](https://github.com/testcontainers/testcontainers-java/pull/526)) ## [1.5.0] - 2017-12-12 ### Fixed - Fixed problems with using container based docker-compose on Windows ([\#514](https://github.com/testcontainers/testcontainers-java/pull/514)) - Fixed problems with copying files on Windows ([\#514](https://github.com/testcontainers/testcontainers-java/pull/514)) - Fixed regression in 1.4.3 when using Docker Compose on Windows ([\#439](https://github.com/testcontainers/testcontainers-java/issues/439)) - Fixed local Docker Compose executable name resolution on Windows ([\#416](https://github.com/testcontainers/testcontainers-java/issues/416)) - Fixed TAR composition on Windows ([\#444](https://github.com/testcontainers/testcontainers-java/issues/444)) - Allowing `addExposedPort` to be used after ports have been specified with `withExposedPorts` ([\#453](https://github.com/testcontainers/testcontainers-java/issues/453)) - Stopping creation of temporary directory prior to creating temporary file ([\#443](https://github.com/testcontainers/testcontainers-java/issues/443)) - Ensure that temp files are created in a temp directory ([\#423](https://github.com/testcontainers/testcontainers-java/issues/423)) - Added `WaitAllStrategy` as a mechanism for composing multiple startup `WaitStrategy` objects together - Changed `BrowserWebDriverContainer` to use improved wait strategies, to eliminate race conditions when starting VNC recording containers. This should lead to far fewer 'error' messages logged when starting up selenium containers, and less exposure to race related bugs (fixes [\#466](https://github.com/testcontainers/testcontainers-java/issues/466)). ### Changed - Make Network instances reusable (i.e. work with `@ClassRule`) ([\#469](https://github.com/testcontainers/testcontainers-java/issues/469)) - Added support for explicitly setting file mode when copying file into container ([\#446](https://github.com/testcontainers/testcontainers-java/issues/446), [\#467](https://github.com/testcontainers/testcontainers-java/issues/467)) - Use Visible Assertions 2.1.0 for pre-flight test output (eliminating Jansi/JNR-POSIX dependencies for lower likelihood of conflict. JNA is now used internally by Visible Assertions instead). - Mark all links functionality as deprecated. This is pending removal in a later release. Please see [\#465](https://github.com/testcontainers/testcontainers-java/issues/465). `Network` features should be used instead. - Added support for copying files to/from running containers ([\#378](https://github.com/testcontainers/testcontainers-java/issues/378)) - Add `getLivenessCheckPorts` as an eventual replacement for `getLivenessCheckPort`; this allows multiple ports to be included in post-startup wait strategies. - Refactor wait strategy port checking and improve test coverage. - Added support for customising the recording file name ([\#500](https://github.com/testcontainers/testcontainers-java/issues/500)) ## [1.4.3] - 2017-10-14 ### Fixed - Fixed local Docker Compose executable name resolution on Windows ([\#416](https://github.com/testcontainers/testcontainers-java/issues/416), [\#460](https://github.com/testcontainers/testcontainers-java/issues/460)) - Fixed TAR composition on Windows ([\#444](https://github.com/testcontainers/testcontainers-java/issues/444)) - Allowing `addExposedPort` to be used after ports have been specified with `withExposedPorts` ([\#453](https://github.com/testcontainers/testcontainers-java/issues/453)) - Stopping creation of temporary directory prior to creating temporary file ([\#443](https://github.com/testcontainers/testcontainers-java/issues/443)) ### Changed - Added `forResponsePredicate` method to HttpWaitStrategy to test response body ([\#441](https://github.com/testcontainers/testcontainers-java/issues/441)) - Changed `DockerClientProviderStrategy` to be loaded via Service Loader ([\#434](https://github.com/testcontainers/testcontainers-java/issues/434), [\#435](https://github.com/testcontainers/testcontainers-java/issues/435)) - Made it possible to specify docker compose container in configuration ([\#422](https://github.com/testcontainers/testcontainers-java/issues/422), [\#425](https://github.com/testcontainers/testcontainers-java/issues/425)) - Clarified wording of pre-flight check messages ([\#457](https://github.com/testcontainers/testcontainers-java/issues/457), [\#436](https://github.com/testcontainers/testcontainers-java/issues/436)) - Added caching of failure to find a docker daemon, so that subsequent tests fail fast. This is likely to be a significant improvement in situations where there is no docker daemon available, dramatically reducing run time and log output when further attempts to find the docker daemon cannot succeed. - Allowing JDBC containers' username, password and DB name to be customized ([\#400](https://github.com/testcontainers/testcontainers-java/issues/400), [\#354](https://github.com/testcontainers/testcontainers-java/issues/354)) ## [1.4.2] - 2017-07-25 ### Fixed - Worked around incompatibility between Netty's Unix socket support and OS X 10.11. Reinstated use of TCP-Unix Socket proxy when running on OS X prior to v10.12. (Fixes [\#402](https://github.com/testcontainers/testcontainers-java/issues/402)) - Changed to use version 2.0 of the Visible Assertions library for startup pre-flight checks. This no longer has a dependency on Jansi, and is intended to resolve a JVM crash issue apparently caused by native lib version conflicts ([\#395](https://github.com/testcontainers/testcontainers-java/issues/395)). Please note that the newer ANSI code is less mature and thus has had less testing, particularly in interesting terminal environments such as Windows. If issues are encountered, coloured assertion output may be disabled by setting the system property `visibleassertions.ansi.enabled` to `true`. - Fixed NullPointerException when calling GenericContainer#isRunning on not started container ([\#411](https://github.com/testcontainers/testcontainers-java/issues/411)) ### Changed - Removed Guava usage from `jdbc` module ([\#401](https://github.com/testcontainers/testcontainers-java/issues/401)) ## [1.4.1] - 2017-07-10 ### Fixed - Fixed Guava shading in `jdbc` module ## [1.4.0] - 2017-07-09 ### Fixed - Fixed the case when disk's size is bigger than Integer's max value ([\#379](https://github.com/testcontainers/testcontainers-java/issues/379), [\#380](https://github.com/testcontainers/testcontainers-java/issues/380)) - Fixed erroneous version reference used during CI testing of shaded dependencies - Fixed leakage of Vibur and Tomcat JDBC test dependencies in `jdbc-test` and `mysql` modules ([\#382](https://github.com/testcontainers/testcontainers-java/issues/382)) - Added timeout and retries for creation of `RemoteWebDriver` ([\#381](https://github.com/testcontainers/testcontainers-java/issues/381), [\#373](https://github.com/testcontainers/testcontainers-java/issues/373), [\#257](https://github.com/testcontainers/testcontainers-java/issues/257)) - Fixed various shading issues - Improved removal of containers/networks when using Docker Compose, eliminating irrelevant errors during cleanup ([\#342](https://github.com/testcontainers/testcontainers-java/issues/342), [\#394](https://github.com/testcontainers/testcontainers-java/issues/394)) ### Changed - Added support for Docker networks ([\#372](https://github.com/testcontainers/testcontainers-java/issues/372)) - Added `getFirstMappedPort` method ([\#377](https://github.com/testcontainers/testcontainers-java/issues/377)) - Extracted Oracle XE container into a separate repository ([testcontainers/testcontainers-java-module-oracle-xe](https://github.com/testcontainers/testcontainers-java-module-oracle-xe)) - Added shading tests - Updated docker-java to 3.0.12 ([\#393](https://github.com/testcontainers/testcontainers-java/issues/393)) ## [1.3.1] - 2017-06-22 ### Fixed - Fixed non-POSIX fallback for file attribute reading ([\#371](https://github.com/testcontainers/testcontainers-java/issues/371)) - Fixed NullPointerException in AuditLogger when running using slf4j-log4j12 bridge ([\#375](https://github.com/testcontainers/testcontainers-java/issues/375)) - Improved cleanup of JDBC connections during database container startup checks ### Changed - Extracted MariaDB into a separate repository ([\#337](https://github.com/testcontainers/testcontainers-java/issues/337)) - Added `TC_DAEMON` JDBC URL flag to prevent `ContainerDatabaseDriver` from shutting down containers at the time all connections are closed. ([\#359](https://github.com/testcontainers/testcontainers-java/issues/359), [\#360](https://github.com/testcontainers/testcontainers-java/issues/360)) - Added pre-flight checks (can be disabled with `checks.disable` configuration property) ([\#363](https://github.com/testcontainers/testcontainers-java/issues/363)) - Improved startup time by adding dynamic priorities to DockerClientProviderStrategy ([\#362](https://github.com/testcontainers/testcontainers-java/issues/362)) - Added global configuration file `~/.testcontainers.properties` ([\#362](https://github.com/testcontainers/testcontainers-java/issues/362)) - Added container arguments to specify SELinux contexts for mounts ([\#334](https://github.com/testcontainers/testcontainers-java/issues/334)) - Removed unused Jersey dependencies ([\#361](https://github.com/testcontainers/testcontainers-java/issues/361)) - Removed deprecated, wrongly-generated setters from `GenericContainer` ## [1.3.0] - 2017-06-05 ### Fixed - Improved container cleanup if startup failed ([\#336](https://github.com/testcontainers/testcontainers-java/issues/336), [\#335](https://github.com/testcontainers/testcontainers-java/issues/335)) ### Changed - Upgraded docker-java library to 3.0.10 ([\#349](https://github.com/testcontainers/testcontainers-java/issues/349)) - Added basic audit logging of Testcontainers' actions via a specific SLF4J logger name with metadata captured via MDC. Intended for use in highly shared Docker environments. - Use string-based detection of Selenium container startup ([\#328](https://github.com/testcontainers/testcontainers-java/issues/328), [\#351](https://github.com/testcontainers/testcontainers-java/issues/351)) - Use string-based detection of PostgreSQL container startup ([\#327](https://github.com/testcontainers/testcontainers-java/issues/327), [\#317](https://github.com/testcontainers/testcontainers-java/issues/317)) - Update libraries to recent versions ([\#333](https://github.com/testcontainers/testcontainers-java/issues/333)) - Introduce abstraction over files and classpath resources, allowing recursive copying of directories ([\#313](https://github.com/testcontainers/testcontainers-java/issues/313)) ## [1.2.1] - 2017-04-06 ### Fixed - Fix bug in space detection when `alpine:3.5` image has not yet been pulled ([\#323](https://github.com/testcontainers/testcontainers-java/issues/323), [\#324](https://github.com/testcontainers/testcontainers-java/issues/324)) - Minor documentation fixes ### Changed - Add AOP Alliance dependencies to shaded deps to reduce chance of conflicts ([\#315](https://github.com/testcontainers/testcontainers-java/issues/315)) ## [1.2.0] - 2017-03-12 ### Fixed - Fix various escaping issues that may arise when paths contain spaces ([\#263](https://github.com/testcontainers/testcontainers-java/issues/263), [\#279](https://github.com/testcontainers/testcontainers-java/issues/279)) - General documentation fixes/improvements ([\#300](https://github.com/testcontainers/testcontainers-java/issues/300), [\#303](https://github.com/testcontainers/testcontainers-java/issues/303), [\#304](https://github.com/testcontainers/testcontainers-java/issues/304)) - Improve reliability of `ResourceReaper` when there are a large number of containers returned by `docker ps -a` ([\#295](https://github.com/testcontainers/testcontainers-java/issues/295)) ### Changed - Support Docker for Windows via TCP socket connection ([\#291](https://github.com/testcontainers/testcontainers-java/issues/291), [\#297](https://github.com/testcontainers/testcontainers-java/issues/297), [\#309](https://github.com/testcontainers/testcontainers-java/issues/309)). _Note that Docker Compose is not yet supported under Docker for Windows (see [\#306](https://github.com/testcontainers/testcontainers-java/issues/306)) - Expose `docker-java`'s `CreateContainerCmd` API for low-level container tweaking ([\#301](https://github.com/testcontainers/testcontainers-java/issues/301)) - Shade `org.newsclub` and Guava dependencies ([\#299](https://github.com/testcontainers/testcontainers-java/issues/299), [\#292](https://github.com/testcontainers/testcontainers-java/issues/292)) - Add `org.testcontainers` label to all containers created by Testcontainers ([\#294](https://github.com/testcontainers/testcontainers-java/issues/294)) ## [1.1.9] - 2017-02-12 ### Fixed - Fix inability to run Testcontainers on Alpine linux. Unix-socket-over-TCP is now used in linux environments where netty fails due to lack of glibc libraries ([\#290](https://github.com/testcontainers/testcontainers-java/issues/290)) - Fix slow feedback in the case of missing JDBC drivers by failing-fast if the required driver cannot be found ([\#280](https://github.com/testcontainers/testcontainers-java/issues/280), [\#230](https://github.com/testcontainers/testcontainers-java/issues/230)) ### Changed - Add ability to change 'tiny image' used for disk space checks ([\#287](https://github.com/testcontainers/testcontainers-java/issues/287)) - Add ability to attach volumes to a container using 'volumes from' ([\#244](https://github.com/testcontainers/testcontainers-java/issues/244), [\#289](https://github.com/testcontainers/testcontainers-java/issues/289)) ## [1.1.8] - 2017-01-22 ### Fixed - Compatibility fixes for Docker for Mac v1.13.0 ([\#272](https://github.com/testcontainers/testcontainers-java/issues/272)) - Relax docker environment disk space check to accommodate unusual empty `df` output observed on Docker for Mac with OverlayFS ([\#273](https://github.com/testcontainers/testcontainers-java/issues/273), [\#278](https://github.com/testcontainers/testcontainers-java/issues/278)) - Fix inadvertent private-scoping of startup checks' `StartupStatus`, which made implementation of custom startup checks impossible ([\#266](https://github.com/testcontainers/testcontainers-java/issues/266)) - Fix potential resource lead/deadlock when errors are encountered building images from a Dockerfile ([\#274](https://github.com/testcontainers/testcontainers-java/issues/274)) ### Changed - Add support for execution within a Docker container ([\#267](https://github.com/testcontainers/testcontainers-java/issues/267)), correcting resolution of container addresses - Add support for version 2 of private docker registries, configured via `$HOME/.docker/config.json` ([\#270](https://github.com/testcontainers/testcontainers-java/issues/270)) - Use current classloader instead of system classloader for loading JDBC drivers ([\#261](https://github.com/testcontainers/testcontainers-java/issues/261)) - Allow hardcoded container image names for Ambassador and VNC recorder containers to be changed via a configuration file ([\#277](https://github.com/testcontainers/testcontainers-java/issues/277), [\#259](https://github.com/testcontainers/testcontainers-java/issues/259)) - Allow Selenium Webdriver container image name to be specified as a constructor parameter ([\#249](https://github.com/testcontainers/testcontainers-java/issues/249), [\#171](https://github.com/testcontainers/testcontainers-java/issues/171)) ## [1.1.7] - 2016-11-19 ### Fixed - Compensate for premature TCP socket opening in Docker for Mac ([\#160](https://github.com/testcontainers/testcontainers-java/issues/160), [\#236](https://github.com/testcontainers/testcontainers-java/issues/236)) - (Internal) Stabilise various parts of Testcontainers' self test suite ([\#241](https://github.com/testcontainers/testcontainers-java/issues/241)) - Fix mounting of classpath resources when those resources are in a JAR file ([\#213](https://github.com/testcontainers/testcontainers-java/issues/213)) - Reduce misleading error messages caused mainly by trying to perform operations on stopped containers ([\#243](https://github.com/testcontainers/testcontainers-java/issues/243)) ### Changed - Uses a default MySQL and MariaDB configuration to reduce memory footprint ([\#209](https://github.com/testcontainers/testcontainers-java/issues/209), [\#243](https://github.com/testcontainers/testcontainers-java/issues/243)) - Docker Compose can optionally now use a local `docker-compose` executable rather than running inside a container ([\#200](https://github.com/testcontainers/testcontainers-java/issues/200)) - Add support for privileged mode containers ([\#234](https://github.com/testcontainers/testcontainers-java/issues/234), [\#235](https://github.com/testcontainers/testcontainers-java/issues/235)) - Allow container/network cleanup (ResourceReaper) to be triggered programmatically ([\#231](https://github.com/testcontainers/testcontainers-java/issues/231)) - Add optional tailing of logs for containers spawned by Docker Compose ([\#233](https://github.com/testcontainers/testcontainers-java/issues/233)) - (Internal) Relocate non-proprietary database container tests to a single module ## [1.1.6] - 2016-09-22 ### Fixed - Fix logging of discovered Docker environment variables ([\#218](https://github.com/testcontainers/testcontainers-java/issues/218)) - Adopt longer timeout periods for testing docker client configurations, and allow these to be further customised through system properties ([\#217](https://github.com/testcontainers/testcontainers-java/issues/217), see *ClientProviderStrategy classes) - Fix docker compose directory mounting on windows ([\#224](https://github.com/testcontainers/testcontainers-java/issues/224)) - Handle and ignore further categories of failure in retrieval of docker environment disk space ([\#225](https://github.com/testcontainers/testcontainers-java/issues/225)) ### Changed - Add extra configurability options (database name, username, password) for PostgreSQL DB containers ([\#220](https://github.com/testcontainers/testcontainers-java/issues/220)) - Add MariaDB container type ([\#215](https://github.com/testcontainers/testcontainers-java/issues/215)) - Use Docker Compose `down` action for more robust teardown of compose environments - Ensure that Docker Compose operations run sequentially rather than concurrently if JUnit tests are parallelized ([\#226](https://github.com/testcontainers/testcontainers-java/issues/226)) - Allow multiple Docker Compose files to be specified, to allow for extension/composition of services ([\#227](https://github.com/testcontainers/testcontainers-java/issues/227)) ## [1.1.5] - 2016-08-22 ### Fixed - Fix Docker Compose environment variable passthrough ([\#208](https://github.com/testcontainers/testcontainers-java/issues/208)) ### Changed - Remove Docker Compose networks when containers are shut down ([\#211](https://github.com/testcontainers/testcontainers-java/issues/211)) as well as at JVM shutdown ## [1.1.4] - 2016-08-16 ### Fixed - Fix JDBC proxy driver behaviour when used with Tomcat connection pool to avoid spawning excessive numbers of containers ([\#195](https://github.com/testcontainers/testcontainers-java/issues/195)) - Shade Jersey dependencies in JDBC module to avoid classpath conflicts ([\#202](https://github.com/testcontainers/testcontainers-java/issues/202)) - Fix NullPointerException when docker host has untagged images ([\#201](https://github.com/testcontainers/testcontainers-java/issues/201)) - Fix relative paths for volumes mounted in docker-compose containers ([\#189](https://github.com/testcontainers/testcontainers-java/issues/189)) ### Changed - Update to v3.0.2 of docker-java library - Switch to a shared, single instance docker client rather than a separate client instance per container rule ([\#193](https://github.com/testcontainers/testcontainers-java/issues/193)) - Ensure that docker-compose pulls images (with no timeout), prior to trying to start ([\#188](https://github.com/testcontainers/testcontainers-java/issues/188)) - Use official `docker/compose` image for running docker-compose ([\#190](https://github.com/testcontainers/testcontainers-java/issues/190)) ## [1.1.3] - 2016-07-27 ### Fixed - Further fix for shading of netty Linux native libs, specifically when run using Docker Compose support - Ensure that file mode permissions are retained for Dockerfile builder ### Changed - Add support for specifying container working directory, and set this to match the `/compose` directory for Docker Compose - Improve resilience of Selenium container startup - Add `withLogConsumer(...)` to allow a log consumer to be attached to a container from the moment of startup ## [1.1.2] - 2016-07-19 ### Fixed - Fix shading of netty Linux native libs ### Changed - Shade guava artifacts to prevent classloader conflicts ## [1.1.1] - 2016-07-17 ### Fixed - Improve shutdown of unnecessary docker clients ([\#170](https://github.com/testcontainers/testcontainers-java/issues/170)) - Shade `io.netty` dependencies into the testcontainers core JAR to reduce conflicts ([\#170](https://github.com/testcontainers/testcontainers-java/issues/170) and [\#157](https://github.com/testcontainers/testcontainers-java/issues/157)) - Remove timeouts for docker compose execution, particularly useful when image pulls are involved - Improve output logging from docker-compose, pausing to log output in case of failure rather than letting logs intermingle. ### Changed - Reinstate container startup retry (removed in v1.1.0) as an optional setting, only used by default for Selenium webdriver containers ## [1.1.0] - 2016-07-05 ### Fixed - Apply shade relocation to Jersey repackaged Guava libs - General logging and stability improvements to Docker Compose support - Fix liveness checks to use specific IP address obtained using `getContainerIpAddress()` ### Changed - Integrate interim support for Docker for Mac beta and Docker Machine for Windows. See [docs](docs/index.md) for known limitations. - Add support for Docker Compose v2 and scaling of compose containers - Add support for attaching containers to specific networks. - Allow container environment variables to be set using a Map ## [1.0.5] - 2016-05-02 ### Fixed - Fix problems associated with changes to `tenforce/virtuoso:latest` container, and replace with a pinned version. - Fix build-time dependency on visible-assertions library, which had downstream dependencies that started to break the Testcontainers build. ### Changed - Add support for pluggable wait strategies, i.e. overriding the default TCP connect wait strategy with HTTP ping or any user-defined approach. - Add 'self-typing' to allow easy use of fluent-style options even when `GenericContainer` is subclassed. - Add support for defining extra entries for containers' `/etc/hosts` files. - Add fluent setter for setting file-system file/directory binding ## [1.0.4] - 2016-04-17 ### Fixed - Prevent unnecessary and erroneous reconfiguration of container if startup needs to be retried - Consolidate container cleanup to ensure that ambassador containers used for Docker Compose are cleaned up appropriately - Fix container liveness check port lookup for FixedHostPortGenericContainer. - Upgrade docker-compose container to dduportal/docker-compose:1.6.0 for compatibility with docker compose file format v2. ### Changed - Add `docker exec` support for running commands against running containers - Add support for building container images on the fly from Dockerfiles, including optional Dockerfile builder DSL - Add container name as prefix for container logs that are streamed to SLF4J - Improve container startup failure detection, including adding the option to specify a minimum up time that the container should achieve before being considered started successfully ## [1.0.3] - 2016-03-31 ### Fixed - Resolve issues where containers would not be cleaned up on JVM shutdown if they failed to start correctly - Fix validation problem where docker image names that contained private registry URLs with port number would be rejected - Resolve bug where `docker pull` would try infinitely for a non-existent image name ### Changed - Set startup free disk space check to ensure that the Docker environment has a minimum of 2GB available rather than 10% - Add streaming of container logs to SLF4J loggers, capture as Strings, and also the ability to wait for container log content to satisfy an expected predicate - Allow configuration of docker container startup timeout - Add detection of classpath Selenium version, and automatic selection of correct Selenium docker containers for compatibility ## [1.0.2] - 2016-02-27 ### Fixed - If a container fail to start up correctly, startup will now be retried up to a limit of 3 times - Add resilience around `getMappedPort` method to fail fast when a port is not yet mapped, rather than generate misleading errors ### Changed - Add JDBC container module for OpenLink Virtuoso - Add additional debug level logging to aid with diagnosis of docker daemon discovery problems - Add support for using a local Unix socket to connect to the Docker daemon ## [1.0.1] - 2016-02-18 ### Fixed - Remove extraneous service loader entries in the shaded JAR - Upgrade to v2.2.0 of docker-java client library to take advantage of unix socket fixes (see https://github.com/docker-java/docker-java/issues/456) - Validate that docker image names include a tag on creation ### Changed - By default, use docker machine name from `DOCKER_MACHINE_NAME` environment, or `default` if it exists - Allow container ports to map to a fixed port on the host through use of the `FixedHostPortGenericContainer` subclass of `GenericContainer` ## [1.0.0] - 2016-02-07 ### Fixed - Resolve Jersey/Jackson dependency clashes by shading (relocating) a version of these libraries into the core Testcontainers JAR - Improve documentation and logging concerning discovery of Docker daemon ### Changed - Rename container `getIpAddress()` method to `getContainerIpAddress()` and deprecate original method name. - Rename container `getHostIpAddress()` method to `getTestHostIpAddress()` ## [0.9.9] - 2016-01-12 ### Fixed - Resolve thread safety issues associated with use of a singleton docker client - Resolve disk space check problems when running on a Debian-based docker host - Fix CircleCI problems where the build could hit memory limits ### Changed - Remove bundled logback.xml to allow users more control over logging - Add Travis CI support for improved breadth of testing ## [0.9.8] - 2015-08-12 ### Changed - Change from Spotify docker client library to docker-java, for improved compatibility with latest versions of Docker - Change from JDK 1.7 minimum requirement to JDK 1.8 - Replace boot2docker support with docker-machine support - Docker images are now prefetched when a @Rule is instantiated - Combined Rule and Container classes throughout, for a reduced set of public classes and removal of some duplication - Improvements to container cleanup, especially removal of data volumes - General improvements to error handling, logging etc throughout ### Added - Docker Compose support - Automatic docker environment disk space check ## [0.9.7] - 2015-08-07 ### Added - Support for overriding MySQL container configuration (my.cnf file overrides) ### Changed - Replace dependency on org.testpackage with org.rnorth.visible-assertions ## [0.9.6] - 2015-07-22 ### Added - Generic container support (allows use of any docker image) using a GenericContainerRule. ### Changed - Renamed from org.rnorth.test-containers to org.testcontainers - Explicit support for usage on linux and use with older versions of Docker (v1.2.0 tested) ## [0.9.5] - 2015-06-28 ### Added - Oracle XE container support ### Changed - Support for JDK 1.7 (previously was JDK 1.8+) ## [0.9.4] and 0.9.3 - 2015-06-23 ### Changed - Refactored for better modularization ## [0.9.2] - 2015-06-13 ### Added - 'Sidekick' VNC recording container to record video of Selenium test sessions ### Changed - Alter timezone used for time display inside Selenium containers ## [0.9.1] - 2015-06-07 ### Added - Support for Selenium webdriver containers - Recording of Selenium test sessions using vnc2flv ## [0.9] - 2015-04-29 Initial release [1.5.1]: https://github.com/testcontainers/testcontainers-java/releases/tag/1.5.1 [1.5.0]: https://github.com/testcontainers/testcontainers-java/releases/tag/1.5.0 [1.4.2]: https://github.com/testcontainers/testcontainers-java/releases/tag/1.4.3 [1.4.2]: https://github.com/testcontainers/testcontainers-java/releases/tag/1.4.2 [1.4.1]: https://github.com/testcontainers/testcontainers-java/releases/tag/1.4.1 [1.4.0]: https://github.com/testcontainers/testcontainers-java/releases/tag/1.4.0 [1.2.0]: https://github.com/testcontainers/testcontainers-java/releases/tag/testcontainers-1.2.0 [1.1.9]: https://github.com/testcontainers/testcontainers-java/releases/tag/testcontainers-1.1.9 [1.1.8]: https://github.com/testcontainers/testcontainers-java/releases/tag/testcontainers-1.1.8 [1.1.7]: https://github.com/testcontainers/testcontainers-java/releases/tag/testcontainers-1.1.7 [1.1.6]: https://github.com/testcontainers/testcontainers-java/releases/tag/testcontainers-1.1.6 [1.1.5]: https://github.com/testcontainers/testcontainers-java/releases/tag/testcontainers-1.1.5 [1.1.4]: https://github.com/testcontainers/testcontainers-java/releases/tag/testcontainers-1.1.4 [1.1.3]: https://github.com/testcontainers/testcontainers-java/releases/tag/testcontainers-1.1.3 [1.1.2]: https://github.com/testcontainers/testcontainers-java/releases/tag/testcontainers-1.1.2 [1.1.1]: https://github.com/testcontainers/testcontainers-java/releases/tag/testcontainers-1.1.1 [1.1.0]: https://github.com/testcontainers/testcontainers-java/releases/tag/testcontainers-1.1.0 [1.0.5]: https://github.com/testcontainers/testcontainers-java/releases/tag/testcontainers-1.0.5 [1.0.4]: https://github.com/testcontainers/testcontainers-java/releases/tag/testcontainers-1.0.4 [1.0.3]: https://github.com/testcontainers/testcontainers-java/releases/tag/testcontainers-1.0.3 [1.0.2]: https://github.com/testcontainers/testcontainers-java/releases/tag/testcontainers-1.0.2 [1.0.1]: https://github.com/testcontainers/testcontainers-java/releases/tag/testcontainers-1.0.1 [1.0.0]: https://github.com/testcontainers/testcontainers-java/releases/tag/testcontainers-1.0.0 [0.9.9]: https://github.com/testcontainers/testcontainers-java/releases/tag/testcontainers-0.9.9 [0.9.8]: https://github.com/testcontainers/testcontainers-java/releases/tag/testcontainers-0.9.8 [0.9.7]: https://github.com/testcontainers/testcontainers-java/releases/tag/test-containers-0.9.7 [0.9.6]: https://github.com/testcontainers/testcontainers-java/releases/tag/test-containers-0.9.6 [0.9.5]: https://github.com/testcontainers/testcontainers-java/releases/tag/test-containers-0.9.5 [0.9.4]: https://github.com/testcontainers/testcontainers-java/releases/tag/test-containers-0.9.4 [0.9.3]: https://github.com/testcontainers/testcontainers-java/releases/tag/test-containers-0.9.3 [0.9.2]: https://github.com/testcontainers/testcontainers-java/releases/tag/test-containers-0.9.2 [0.9.1]: https://github.com/testcontainers/testcontainers-java/releases/tag/test-containers-0.9.1 [0.9]: https://github.com/testcontainers/testcontainers-java/releases/tag/test-containers-0.9 ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Please see the [main contributing guidelines](./docs/contributing.md). There are additional docs describing [contributing documentation changes](./docs/contributing_docs.md). ### GitHub Sponsorship Testcontainers is [in the GitHub Sponsors program](https://github.com/sponsors/testcontainers)! This repository is supported by our sponsors, meaning that issues are eligible to have a 'bounty' attached to them by sponsors. Please see [the bounty policy page](https://www.testcontainers.org/bounty) if you are interested, either as a sponsor or as a contributor. ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015-2019 Richard North Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Testcontainers [![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) > Testcontainers is a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container. ![Testcontainers logo](docs/logo.png) # [Read the documentation here](https://java.testcontainers.org) ## License See [LICENSE](LICENSE). ## Copyright Copyright (c) 2015 - 2021 Richard North and other authors. MS SQL Server module is (c) 2017 - 2021 G DATA Software AG and other authors. Hashicorp Vault module is (c) 2017 - 2021 Capital One Services, LLC and other authors. See [contributors](https://github.com/testcontainers/testcontainers-java/graphs/contributors) for all contributors. ================================================ FILE: RELEASING.md ================================================ # Release process Testcontainers' release process is semi-automated through GitHub Actions. This describes the basic steps for a project member to perform a release. ## Steps 1. Ensure that the `main` branch is building and that tests are passing. 1. Create a new release on GitHub. **The tag name is used as the version**, so please keep the tag name plain (e.g. 1.2.3). 1. The release triggers a GitHub Action workflow. 1. Log in to [Sonatype](https://oss.sonatype.org/) to check the staging repository. * Getting access to Sonatype requires a Sonatype JIRA account and [raising an issue](https://issues.sonatype.org/browse/OSSRH-74229), requesting access. 3. Get the staging URL from Sonatype after GitHub Action workflow finished. The general URL format should be `https://oss.sonatype.org/service/local/repositories/$staging-repo-id/content/` 4. Manually test the release with the staging URL as maven repository URL (e.g. critical issues and features). 5. Run [TinSalver](https://github.com/bsideup/tinsalver) from GitHub using `npx` to sign artifact (see [TinSalver README](https://github.com/bsideup/tinsalver/blob/main/README.md)). * For TinSalver to correctly work with keybase on WSL on Windows, you might need to disable pinentry: `keybase config set -b pinentry.disabled true`. 7. Close the release in Sonatype. This will evaluate the release based on given Sonatype rules. 8. After successful closing, the release button needs to be clicked and afterwards it is automatically synced to Maven Central. 9. Handcraft and polish some of the release notes (e.g. substitute combined dependency PRs and highlight certain features). 10. Rename existing milestone corresponding to new release and close it. Then create a new `next` milestone. 11. When available through Maven Central, poke [Richard North](https://github.com/rnorth) to announce the release on Twitter! 12. Merge automated version update PRs in order to update the reference version in `mkdocs.yml` and `gradle.properties`. ## Internal details * The process is done with GitHub Actions, TinSalver and Sonatype. * Sonatype will automatically promote the staging release to Maven Central. * Keybase needs to be installed on the developer machine. * GPG key of signing developer needs to be uploaded to the [Ubuntu keyserver](https://keyserver.ubuntu.com/) (or other server supported by Sonatype). ================================================ FILE: annotations/com/google/common/base/annotations.xml ================================================ ================================================ FILE: annotations/org/rnorth/ducttape/annotations.xml ================================================ ================================================ FILE: azure-pipelines.yml ================================================ jobs: - job: core_tests timeoutInMinutes: 60 steps: # Run all core tests when running the Windows CI tests - task: Gradle@2 condition: eq(variables['Agent.OS'], 'Windows_NT') displayName: Build & test (Windows - core) env: AWS_ACCESS_KEY_ID: $(aws.accessKeyId) AWS_SECRET_ACCESS_KEY: $(aws.secretAccessKey) inputs: gradleWrapperFile: 'gradlew' jdkVersionOption: '1.8' options: '--no-daemon --continue' tasks: 'clean testcontainers:check' publishJUnitResults: true testResultsFiles: '**/TEST-*.xml' - job: other_tests timeoutInMinutes: 120 steps: # Run all non-core tests when running the Windows CI tests - task: Gradle@2 condition: eq(variables['Agent.OS'], 'Windows_NT') displayName: Build & test (Windows - all non-core modules) env: AWS_ACCESS_KEY_ID: $(aws.accessKeyId) AWS_SECRET_ACCESS_KEY: $(aws.secretAccessKey) inputs: gradleWrapperFile: 'gradlew' jdkVersionOption: '1.8' options: '--no-daemon --continue' tasks: 'clean check -x testcontainers:test' publishJUnitResults: true testResultsFiles: '**/TEST-*.xml' ================================================ FILE: bom/build.gradle ================================================ description = "Testcontainers :: BOM" publishing { publications { mavenJava(MavenPublication) { publication -> artifactId = "testcontainers-bom" artifacts = [] pom.withXml { def dependencyManagementNode = asNode().appendNode('dependencyManagement').appendNode('dependencies') def bomProject = project rootProject.subprojects.each { subProject -> if (subProject != bomProject && subProject.plugins.findPlugin("maven-publish")) { dependencyManagementNode.appendNode('dependency').with { appendNode('groupId', subProject.group) appendNode('artifactId',subProject.name) appendNode('version', subProject.version) } } } } } } } ================================================ FILE: build.gradle ================================================ buildscript { repositories { mavenCentral() } dependencies { // https://github.com/melix/japicmp-gradle-plugin/issues/36 classpath 'com.google.guava:guava:33.3.1-jre' classpath 'com.github.tjni.captainhook:captain-hook:0.1.5' } } plugins { id 'io.franzbecker.gradle-lombok' version '5.0.0' id 'com.gradleup.shadow' version '8.3.9' id 'me.champeau.gradle.japicmp' version '0.4.3' apply false id 'com.diffplug.spotless' version '6.22.0' apply false id 'org.jreleaser' version '1.20.0' apply false } apply from: "$rootDir/gradle/ci-support.gradle" apply plugin: 'com.github.tjni.captainhook' captainHook { autoApplyGitHooks = Boolean.valueOf(System.getenv("AUTO_APPLY_GIT_HOOKS")) preCommit = './gradlew spotlessApply' } subprojects { apply plugin: 'java' apply plugin: 'java-library' apply plugin: 'idea' apply plugin: 'io.franzbecker.gradle-lombok' apply from: "$rootDir/gradle/shading.gradle" apply from: "$rootDir/gradle/spotless.gradle" apply plugin: 'checkstyle' group = "org.testcontainers" java { toolchain { languageVersion = JavaLanguageVersion.of(17) } } tasks.withType(JavaCompile) { options.release.set(8) options.encoding = 'UTF-8' } compileTestJava.options.encoding = 'UTF-8' javadoc.options.encoding = 'UTF-8' repositories { mavenCentral() } configurations { provided api.extendsFrom(provided) } lombok { version = '1.18.30' } task delombok(type: io.franzbecker.gradle.lombok.task.DelombokTask) { outputs.cacheIf { true } argumentProviders.addAll( new org.testcontainers.build.DelombokArgumentProvider(srcDirs: project.sourceSets.main.java.srcDirs, outputDir: file("$buildDir/delombok")) ) } delombok.onlyIf { project.sourceSets.main.java.srcDirs.find { it.exists() } } // specific modules should be excluded from publication if ( ! ["test-support", "testcontainers-jdbc-test"].contains(it.name) && !it.path.startsWith(":docs:") && it != project(":docs") ) { apply from: "$rootDir/gradle/publishing.gradle" if (it.name != "bom") { apply plugin: "me.champeau.gradle.japicmp" tasks.register('japicmp', me.champeau.gradle.japicmp.JapicmpTask) apply from: "$rootDir/gradle/japicmp.gradle" } } test { useJUnitPlatform() defaultCharacterEncoding = "UTF-8" testLogging { displayGranularity 1 showStackTraces = true exceptionFormat = 'full' events "STARTED", "PASSED", "FAILED", "SKIPPED" } ext.isCI = System.getenv("CI") != null if (isCI) { develocity.testRetry { maxRetries = 2 maxFailures = 5 failOnPassedAfterRetry = false } } } tasks.withType(Test).all { reports { junitXml.outputPerTestCase = true } } // Ensure that Javadoc generation is always tested check.dependsOn(javadoc) def postCheckCommand = properties["postCheckCommand"] if (postCheckCommand) { check.finalizedBy(tasks.create("postCheckExec", Exec) { if (org.gradle.internal.os.OperatingSystem.current().isWindows()) { commandLine('cmd', '/c', postCheckCommand) } else { commandLine('sh', '-c', postCheckCommand) } }) } javadoc { dependsOn delombok source = delombok.outputs } dependencies { testImplementation 'ch.qos.logback:logback-classic:1.3.15' testImplementation 'org.assertj:assertj-core:3.27.4' testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.11.0' } checkstyle { toolVersion = "10.23.0" configFile = rootProject.file('config/checkstyle/checkstyle.xml') } } ================================================ FILE: buildSrc/build.gradle ================================================ plugins { id 'java-gradle-plugin' } repositories { mavenCentral() } ================================================ FILE: buildSrc/src/main/groovy/org/testcontainers/build/ComparePOMWithLatestReleasedTask.groovy ================================================ package org.testcontainers.build import groovy.xml.XmlSlurper import org.gradle.api.DefaultTask import org.gradle.api.tasks.Input import org.gradle.api.tasks.TaskAction class ComparePOMWithLatestReleasedTask extends DefaultTask { @Input Set ignore = [] @TaskAction def doCompare() { def rootNode = new XmlSlurper().parse(project.tasks.generatePomFileForMavenJavaPublication.destination) def artifactId = rootNode.artifactId.text() def latestRelease = new XmlSlurper() .parse("https://repo1.maven.org/maven2/org/testcontainers/${artifactId}/maven-metadata.xml") .versioning.release.text() def releasedRootNode = new XmlSlurper() .parse("https://repo1.maven.org/maven2/org/testcontainers/${artifactId}/${latestRelease}/${artifactId}-${latestRelease}.pom") Set dependencies = releasedRootNode.dependencies.children() .collect { "${it.groupId.text()}:${it.artifactId.text()}".toString() } for (dependency in rootNode.dependencies.children()) { def coordinates = "${dependency.groupId.text()}:${dependency.artifactId.text()}".toString() if (!dependencies.contains(coordinates) && !ignore.contains(coordinates)) { throw new IllegalStateException("A new dependency '${coordinates}' has been added to 'org.testcontainers:${artifactId}' - if this was intentional please add it to the ignore list in ${project.buildFile}") } } } } ================================================ FILE: buildSrc/src/main/groovy/org/testcontainers/build/DelombokArgumentProvider.groovy ================================================ package org.testcontainers.build import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity import org.gradle.process.CommandLineArgumentProvider /** * Allows build cache relocatability for Delombok task */ class DelombokArgumentProvider implements CommandLineArgumentProvider { @InputFiles @PathSensitive(PathSensitivity.RELATIVE) Set srcDirs @OutputDirectory File outputDir @Override Iterable asArguments() { return [srcDirs.collect { it.absolutePath }.join(" "), "-d", outputDir.absolutePath, "-f", "generateDelombokComment:skip"] } } ================================================ FILE: config/checkstyle/checkstyle.xml ================================================ ================================================ FILE: core/build.gradle ================================================ apply plugin: 'com.gradleup.shadow' description = "Testcontainers Core" sourceSets { jarFileTest } test.maxParallelForks = 4 idea.module.testSourceDirs += sourceSets.jarFileTest.allSource.srcDirs jar { manifest { attributes('Implementation-Version': project.getProperty("version")) } } shadowJar { [ 'META-INF/NOTICE', 'META-INF/NOTICE.txt', 'META-INF/LICENSE', 'META-INF/LICENSE.txt', 'META-INF/maven/', 'META-INF/proguard/', 'META-INF/versions/*/module-info.class', 'META-INF/services/java.security.Provider', ].each { exclude(it) } } task jarFileTest(type: Test) { useJUnitPlatform() testClassesDirs = sourceSets.jarFileTest.output.classesDirs classpath = sourceSets.jarFileTest.runtimeClasspath file(shadowJar.outputs.files.singleFile) // input for correct caching systemProperty("jarFile", shadowJar.outputs.files.singleFile) dependsOn(shadowJar) } project.tasks.check.dependsOn(jarFileTest) tasks.japicmp { packageExcludes = [ "com.github.dockerjava.*", "org.testcontainers.shaded.*", ] classExcludes = [] methodExcludes = [] fieldExcludes = [] } configurations.all { resolutionStrategy { force 'com.fasterxml.jackson.core:jackson-databind:2.18.4' force 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.4' } } dependencies { api 'org.slf4j:slf4j-api:1.7.36' compileOnly 'org.jetbrains:annotations:26.0.2-1' testCompileOnly 'org.jetbrains:annotations:26.0.2-1' api 'org.apache.commons:commons-compress:1.28.0' api ('org.rnorth.duct-tape:duct-tape:1.0.8') { exclude(group: 'org.jetbrains', module: 'annotations') } provided('com.google.cloud.tools:jib-core:0.27.3') { exclude group: 'com.google.guava', module: 'guava' exclude group: 'com.fasterxml.jackson.datatype', module: 'jackson-datatype-jsr310' exclude group: 'com.fasterxml.jackson.core', module: 'jackson-core' exclude group: 'com.fasterxml.jackson.core', module: 'jackson-databind' exclude group: 'org.apache.commons', module: 'commons-compress' } shaded 'org.awaitility:awaitility:4.3.0' api platform('com.github.docker-java:docker-java-bom:3.7.1') shaded platform('com.github.docker-java:docker-java-bom:3.7.1') api "com.github.docker-java:docker-java-api" shaded('com.github.docker-java:docker-java-core') { exclude group: 'com.google.guava', module: 'guava' } api 'com.github.docker-java:docker-java-transport-zerodep' shaded 'com.google.guava:guava:33.3.1-jre' shaded "org.yaml:snakeyaml:2.5" shaded 'org.glassfish.main.external:trilead-ssh2-repackaged:4.1.2' shaded 'org.zeroturnaround:zt-exec:1.12' testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.11.0' testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' testImplementation('com.google.cloud.tools:jib-core:0.27.3') { exclude group: 'com.google.guava', module: 'guava' } testImplementation 'org.apache.httpcomponents:httpclient:4.5.14' testImplementation 'redis.clients:jedis:6.2.0' testImplementation 'com.rabbitmq:amqp-client:5.26.0' testImplementation 'org.mongodb:mongo-java-driver:3.12.14' testImplementation ('org.mockito:mockito-core:4.11.0') { exclude(module: 'hamcrest-core') } // Synthetic JAR used for MountableFileTest and DirectoryTarResourceTest testImplementation files('testlib/repo/fakejar/fakejar/0/fakejar-0.jar') testImplementation 'org.assertj:assertj-core:3.27.6' testImplementation 'io.rest-assured:rest-assured:5.5.6' jarFileTestCompileOnly "org.projectlombok:lombok:${lombok.version}" jarFileTestAnnotationProcessor "org.projectlombok:lombok:${lombok.version}" jarFileTestRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.11.0' jarFileTestImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' jarFileTestImplementation 'org.assertj:assertj-core:3.27.6' jarFileTestImplementation 'org.ow2.asm:asm-debug-all:5.2' } tasks.generatePomFileForMavenJavaPublication.finalizedBy( tasks.register('checkPOMdependencies', org.testcontainers.build.ComparePOMWithLatestReleasedTask) { ignore = [ ] } ) compileTestJava { javaCompiler = javaToolchains.compilerFor { languageVersion = JavaLanguageVersion.of(17) } options.release.set(17) } test { useJUnitPlatform() } ================================================ FILE: core/src/jarFileTest/java/org/testcontainers/AbstractJarFileTest.java ================================================ package org.testcontainers; import java.net.URI; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collections; public abstract class AbstractJarFileTest { public static Path root; static { try { Path jarFilePath = Paths.get(System.getProperty("jarFile")); String decodedPath = URLDecoder.decode(jarFilePath.toUri().toString(), StandardCharsets.UTF_8.name()); URI jarFileUri = new URI("jar", decodedPath, null); FileSystem fileSystem = FileSystems.newFileSystem(jarFileUri, Collections.emptyMap()); root = fileSystem.getPath("/"); } catch (Exception e) { throw new RuntimeException(e); } } } ================================================ FILE: core/src/jarFileTest/java/org/testcontainers/JarFileShadingTest.java ================================================ package org.testcontainers; import org.assertj.core.api.ListAssert; import org.junit.jupiter.api.Test; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import static org.assertj.core.api.Assertions.assertThat; class JarFileShadingTest extends AbstractJarFileTest { @Test void testPackages() throws Exception { assertThatFileList(root).containsOnly("org", "META-INF"); assertThatFileList(root.resolve("org")).containsOnly("testcontainers"); } @Test void testMetaInf() throws Exception { assertThatFileList(root.resolve("META-INF")) .containsOnly( "MANIFEST.MF", "services", "versions", "native-image", "thirdparty-LICENSE", "FastDoubleParser-NOTICE", "FastDoubleParser-LICENSE" ); } @Test void testMetaInfServices() throws Exception { assertThatFileList(root.resolve("META-INF").resolve("services")) .allMatch(it -> it.startsWith("org.testcontainers.")); } private ListAssert assertThatFileList(Path path) throws IOException { return (ListAssert) assertThat(Files.list(path)) .extracting(Path::getFileName) .extracting(Path::toString) .extracting(it -> it.endsWith("/") ? it.substring(0, it.length() - 1) : it); } } ================================================ FILE: core/src/jarFileTest/java/org/testcontainers/PublicBinaryAPITest.java ================================================ package org.testcontainers; import lombok.RequiredArgsConstructor; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.Parameter; import org.junit.jupiter.params.ParameterizedClass; import org.junit.jupiter.params.provider.MethodSource; import org.objectweb.asm.ClassReader; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.FieldNode; import org.objectweb.asm.tree.MethodNode; import java.io.IOException; import java.io.InputStream; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; /** * This test checks that we don't expose any shaded class in our public API. */ @ParameterizedClass @MethodSource("data") @RequiredArgsConstructor public class PublicBinaryAPITest extends AbstractJarFileTest { private static final String SHADED_PACKAGE = "org.testcontainers.shaded."; private static final String SHADED_PACKAGE_PATH = SHADED_PACKAGE.replaceAll("\\.", "/"); static { Assertions.registerFormatterForType(ClassNode.class, it -> it.name); Assertions.registerFormatterForType(FieldNode.class, it -> it.name); Assertions.registerFormatterForType(MethodNode.class, it -> it.name + it.desc); } public static List data() throws Exception { List result = new ArrayList<>(); Files.walkFileTree( root, new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException { String fileName = path.toString(); if (!fileName.endsWith(".class")) { return super.visitFile(path, attrs); } if (!fileName.startsWith("/org/testcontainers/")) { return super.visitFile(path, attrs); } if (fileName.startsWith("/" + SHADED_PACKAGE_PATH)) { return super.visitFile(path, attrs); } try (InputStream inputStream = Files.newInputStream(path)) { ClassReader reader = new ClassReader(inputStream); ClassNode node = new ClassNode(); reader.accept(node, ClassReader.SKIP_CODE); if ((node.access & Opcodes.ACC_PUBLIC) != 0) { result.add(new Object[] { fileName, node }); } } return super.visitFile(path, attrs); } } ); return result; } @Parameter(0) private String fileName; @Parameter(1) private ClassNode classNode; @BeforeEach public void setUp() { switch (classNode.name) { // Necessary evil case "org/testcontainers/dockerclient/UnixSocketClientProviderStrategy": case "org/testcontainers/dockerclient/DockerClientProviderStrategy": case "org/testcontainers/dockerclient/WindowsClientProviderStrategy": case "org/testcontainers/utility/DynamicPollInterval": Assumptions.assumeTrue(false); } } @Test void testSuperClass() { assertThat(classNode.superName).doesNotStartWith(SHADED_PACKAGE_PATH); } @Test void testInterfaces() { assertThat(classNode.interfaces).allSatisfy(it -> assertThat(it).doesNotStartWith(SHADED_PACKAGE_PATH)); } @Test void testMethodReturnTypes() { assertThat(classNode.methods) .filteredOn(it -> (it.access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) != 0) .allSatisfy(it -> assertThat(Type.getReturnType(it.desc).getClassName()).doesNotStartWith(SHADED_PACKAGE)); } @Test void testMethodArguments() { assertThat(classNode.methods) .filteredOn(it -> (it.access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) != 0) .allSatisfy(method -> { assertThat(Arrays.asList(Type.getArgumentTypes(method.desc))) .extracting(Type::getClassName) .allSatisfy(it -> assertThat(it).doesNotStartWith(SHADED_PACKAGE)); }); } @Test void testFields() { assertThat(classNode.fields) .filteredOn(it -> (it.access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) != 0) .allSatisfy(it -> assertThat(Type.getType(it.desc).getClassName()).doesNotStartWith(SHADED_PACKAGE)); } } ================================================ FILE: core/src/main/java/org/testcontainers/DelegatingDockerClient.java ================================================ package org.testcontainers; import com.github.dockerjava.api.DockerClient; import lombok.RequiredArgsConstructor; import lombok.experimental.Delegate; @RequiredArgsConstructor class DelegatingDockerClient implements DockerClient { @Delegate private final DockerClient dockerClient; } ================================================ FILE: core/src/main/java/org/testcontainers/DockerClientFactory.java ================================================ package org.testcontainers; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.DockerClientDelegate; import com.github.dockerjava.api.command.CreateContainerCmd; import com.github.dockerjava.api.command.PullImageCmd; import com.github.dockerjava.api.exception.DockerClientException; import com.github.dockerjava.api.exception.InternalServerErrorException; import com.github.dockerjava.api.exception.NotFoundException; import com.github.dockerjava.api.model.AccessMode; import com.github.dockerjava.api.model.Bind; import com.github.dockerjava.api.model.Info; import com.github.dockerjava.api.model.Version; import com.github.dockerjava.api.model.Volume; import com.google.common.annotations.VisibleForTesting; import lombok.Getter; import lombok.SneakyThrows; import lombok.Synchronized; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; import org.testcontainers.dockerclient.DockerClientProviderStrategy; import org.testcontainers.dockerclient.DockerMachineClientProviderStrategy; import org.testcontainers.dockerclient.TransportConfig; import org.testcontainers.images.RemoteDockerImage; import org.testcontainers.images.TimeLimitedLoggedPullImageResultCallback; import org.testcontainers.utility.ComparableVersion; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import org.testcontainers.utility.ResourceReaper; import org.testcontainers.utility.TestcontainersConfiguration; import java.io.InputStream; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.ServiceLoader; import java.util.UUID; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.stream.Collectors; /** * Singleton class that provides initialized Docker clients. *

* The correct client configuration to use will be determined on first use, and cached thereafter. */ @Slf4j public class DockerClientFactory { public static final ThreadGroup TESTCONTAINERS_THREAD_GROUP = new ThreadGroup("testcontainers"); public static final String TESTCONTAINERS_LABEL = DockerClientFactory.class.getPackage().getName(); public static final String TESTCONTAINERS_SESSION_ID_LABEL = TESTCONTAINERS_LABEL + ".sessionId"; public static final String TESTCONTAINERS_LANG_LABEL = TESTCONTAINERS_LABEL + ".lang"; public static final String TESTCONTAINERS_VERSION_LABEL = TESTCONTAINERS_LABEL + ".version"; public static final String SESSION_ID = UUID.randomUUID().toString(); public static final String TESTCONTAINERS_VERSION = DockerClientFactory.class.getPackage().getImplementationVersion(); public static final Map DEFAULT_LABELS = markerLabels(); static Map markerLabels() { String testcontainersVersion = TESTCONTAINERS_VERSION == null ? "unspecified" : TESTCONTAINERS_VERSION; Map labels = new HashMap<>(); labels.put(TESTCONTAINERS_LABEL, "true"); labels.put(TESTCONTAINERS_LANG_LABEL, "java"); labels.put(TESTCONTAINERS_VERSION_LABEL, testcontainersVersion); return Collections.unmodifiableMap(labels); } private static final DockerImageName TINY_IMAGE = DockerImageName.parse("alpine:3.17"); private static DockerClientFactory instance; // Cached client configuration @VisibleForTesting DockerClientProviderStrategy strategy; @VisibleForTesting DockerClient client; @VisibleForTesting RuntimeException cachedClientFailure; private String activeApiVersion; @Getter(lazy = true) private final boolean fileMountingSupported = checkMountableFile(); @VisibleForTesting DockerClientFactory() {} public static DockerClient lazyClient() { return new DockerClientDelegate() { @Override protected DockerClient getDockerClient() { return instance().client(); } @Override public String toString() { return "LazyDockerClient"; } }; } /** * Obtain an instance of the DockerClientFactory. * * @return the singleton instance of DockerClientFactory */ public static synchronized DockerClientFactory instance() { if (instance == null) { instance = new DockerClientFactory(); } return instance; } /** * Checks whether Docker is accessible and {@link #client()} is able to produce a client. * * @return true if Docker is available, false if not. */ public synchronized boolean isDockerAvailable() { try { client(); return true; } catch (IllegalStateException ex) { return false; } } @Synchronized private DockerClientProviderStrategy getOrInitializeStrategy() { if (strategy != null) { return strategy; } log.info("Testcontainers version: {}", DEFAULT_LABELS.get(TESTCONTAINERS_VERSION_LABEL)); List configurationStrategies = new ArrayList<>(); ServiceLoader.load(DockerClientProviderStrategy.class).forEach(configurationStrategies::add); strategy = DockerClientProviderStrategy.getFirstValidStrategy(configurationStrategies); return strategy; } @UnstableAPI public TransportConfig getTransportConfig() { return getOrInitializeStrategy().getTransportConfig(); } @UnstableAPI public String getRemoteDockerUnixSocketPath() { DockerClientProviderStrategy strategy = getOrInitializeStrategy(); if (strategy.allowUserOverrides()) { String dockerSocketOverride = System.getenv("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE"); if (!StringUtils.isBlank(dockerSocketOverride)) { return dockerSocketOverride; } } if (strategy.getRemoteDockerUnixSocketPath() != null) { return strategy.getRemoteDockerUnixSocketPath(); } URI dockerHost = getTransportConfig().getDockerHost(); String path = "unix".equals(dockerHost.getScheme()) ? dockerHost.getRawPath() : "/var/run/docker.sock"; return SystemUtils.IS_OS_WINDOWS ? "/" + path : path; } /** * @return a new initialized Docker client */ @Synchronized public DockerClient client() { // fail-fast if checks have failed previously if (cachedClientFailure != null) { log.debug("There is a cached checks failure - throwing", cachedClientFailure); throw cachedClientFailure; } if (client != null) { return client; } final DockerClientProviderStrategy strategy = getOrInitializeStrategy(); client = new DockerClientDelegate() { @Getter final DockerClient dockerClient = strategy.getDockerClient(); @Override public void close() { throw new IllegalStateException("You should never close the global DockerClient!"); } }; log.info("Docker host IP address is {}", strategy.getDockerHostIpAddress()); Info dockerInfo = strategy.getInfo(); log.debug("Docker info: {}", dockerInfo.getRawValues()); Version version = client.versionCmd().exec(); log.debug("Docker version: {}", version.getRawValues()); activeApiVersion = version.getApiVersion(); String serverInfo = "Connected to docker: \n" + " Server Version: " + dockerInfo.getServerVersion() + "\n" + " API Version: " + activeApiVersion + "\n" + " Operating System: " + dockerInfo.getOperatingSystem() + "\n" + " Total Memory: " + dockerInfo.getMemTotal() / (1024 * 1024) + " MB"; String[] labels = dockerInfo.getLabels(); boolean hasLabels = labels != null && labels.length > 0; if (hasLabels) { String formattedLabels = Arrays .stream(labels) .map(label -> " " + label) .collect(Collectors.joining("\n")); serverInfo += "\n Labels: \n" + formattedLabels; } log.info(serverInfo); try { //noinspection deprecation ResourceReaper.instance().init(); } catch (RuntimeException e) { cachedClientFailure = e; throw e; } boolean checksEnabled = !TestcontainersConfiguration.getInstance().isDisableChecks(); if (checksEnabled) { log.debug("Checks are enabled"); try { log.info("Checking the system..."); checkDockerVersion(version.getVersion()); } catch (RuntimeException e) { cachedClientFailure = e; throw e; } } else { log.debug("Checks are disabled"); } return client; } private void checkDockerVersion(String dockerVersion) { boolean versionIsSufficient = new ComparableVersion(dockerVersion).compareTo(new ComparableVersion("1.6.0")) >= 0; check("Docker server version should be at least 1.6.0", versionIsSufficient); } private void check(String message, boolean isSuccessful) { if (isSuccessful) { log.info("\u2714\ufe0e {}", message); } else { log.error("\u274c {}", message); throw new IllegalStateException("Check failed: " + message); } } private boolean checkMountableFile() { DockerClient dockerClient = client(); MountableFile mountableFile = MountableFile.forClasspathResource( ResourceReaper.class.getName().replace(".", "/") + ".class" ); Volume volume = new Volume("/dummy"); try { return runInsideDocker( createContainerCmd -> { createContainerCmd.withBinds(new Bind(mountableFile.getResolvedPath(), volume, AccessMode.ro)); }, (__, containerId) -> { try ( InputStream stream = dockerClient .copyArchiveFromContainerCmd(containerId, volume.getPath()) .exec() ) { stream.read(); return true; } catch (Exception e) { return false; } } ); } catch (Exception e) { log.debug("Failure while checking for mountable file support", e); return false; } } /** * Check whether the image is available locally and pull it otherwise * * @deprecated use {@link RemoteDockerImage} */ @SneakyThrows @Deprecated public void checkAndPullImage(DockerClient client, String image) { try { client.inspectImageCmd(image).exec(); } catch (NotFoundException notFoundException) { PullImageCmd pullImageCmd = client.pullImageCmd(image); try { pullImageCmd.exec(new TimeLimitedLoggedPullImageResultCallback(log)).awaitCompletion(); } catch (DockerClientException e) { // Try to fallback to x86 pullImageCmd .withPlatform("linux/amd64") .exec(new TimeLimitedLoggedPullImageResultCallback(log)) .awaitCompletion(); } } } /** * @return the IP address of the host running Docker */ public String dockerHostIpAddress() { return getOrInitializeStrategy().getDockerHostIpAddress(); } public T runInsideDocker( Consumer createContainerCmdConsumer, BiFunction block ) { return runInsideDocker(TINY_IMAGE, createContainerCmdConsumer, block); } T runInsideDocker( DockerImageName imageName, Consumer createContainerCmdConsumer, BiFunction block ) { RemoteDockerImage dockerImage = new RemoteDockerImage(imageName); HashMap labels = new HashMap<>(DEFAULT_LABELS); labels.putAll(ResourceReaper.instance().getLabels()); CreateContainerCmd createContainerCmd = client.createContainerCmd(dockerImage.get()).withLabels(labels); createContainerCmdConsumer.accept(createContainerCmd); String id = createContainerCmd.exec().getId(); try { client.startContainerCmd(id).exec(); return block.apply(client, id); } finally { try { client.removeContainerCmd(id).withRemoveVolumes(true).withForce(true).exec(); } catch (NotFoundException | InternalServerErrorException e) { log.debug("Swallowed exception while removing container", e); } } } /** * @return the docker API version of the daemon that we have connected to */ public String getActiveApiVersion() { client(); return activeApiVersion; } /** * @return the docker execution driver of the daemon that we have connected to */ public String getActiveExecutionDriver() { return getInfo().getExecutionDriver(); } /** * @param providerStrategyClass a class that extends {@link DockerMachineClientProviderStrategy} * @return whether or not the currently active strategy is of the provided type */ public boolean isUsing(Class providerStrategyClass) { return strategy != null && providerStrategyClass.isAssignableFrom(this.strategy.getClass()); } @UnstableAPI public Info getInfo() { return getOrInitializeStrategy().getInfo(); } } ================================================ FILE: core/src/main/java/org/testcontainers/Testcontainers.java ================================================ package org.testcontainers; import lombok.experimental.UtilityClass; import org.testcontainers.containers.PortForwardingContainer; import java.util.Map; import java.util.Map.Entry; @UtilityClass public class Testcontainers { public void exposeHostPorts(int... ports) { for (int port : ports) { PortForwardingContainer.INSTANCE.exposeHostPort(port); } } public void exposeHostPorts(Map ports) { for (Entry entry : ports.entrySet()) { PortForwardingContainer.INSTANCE.exposeHostPort(entry.getKey(), entry.getValue()); } } } ================================================ FILE: core/src/main/java/org/testcontainers/UnstableAPI.java ================================================ package org.testcontainers; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Marks that the annotated API is a subject to change and SHOULD NOT be considered * a stable API. */ @Retention(RetentionPolicy.SOURCE) @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD }) @Documented public @interface UnstableAPI { } ================================================ FILE: core/src/main/java/org/testcontainers/containers/BindMode.java ================================================ package org.testcontainers.containers; import com.github.dockerjava.api.model.AccessMode; /** * Possible modes for binding storage volumes. */ public enum BindMode { READ_ONLY(AccessMode.ro), READ_WRITE(AccessMode.rw); public final AccessMode accessMode; BindMode(AccessMode accessMode) { this.accessMode = accessMode; } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/ComposeCommand.java ================================================ package org.testcontainers.containers; import java.util.Set; class ComposeCommand { static String getDownCommand(ComposeDelegate.ComposeVersion composeVersion, Set options) { String composeOptions = optionsAsString(options); if (composeOptions.isEmpty()) { return composeVersion == ComposeDelegate.ComposeVersion.V1 ? "down" : "compose down"; } String cmd = composeVersion == ComposeDelegate.ComposeVersion.V1 ? "%s down" : "compose %s down"; return String.format(cmd, composeOptions); } static String getUpCommand(ComposeDelegate.ComposeVersion composeVersion, Set options) { String composeOptions = optionsAsString(options); if (composeOptions.isEmpty()) { return composeVersion == ComposeDelegate.ComposeVersion.V1 ? "up -d" : "compose up -d"; } String cmd = composeVersion == ComposeDelegate.ComposeVersion.V1 ? "%s up -d" : "compose %s up -d"; return String.format(cmd, composeOptions); } private static String optionsAsString(final Set options) { String optionsString = String.join(" ", options); if (!optionsString.isEmpty()) { // ensures that there is a space between the options and 'up' if options are passed. return optionsString; } else { // otherwise two spaces would appear between 'docker-compose' and 'up' return ""; } } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/ComposeContainer.java ================================================ package org.testcontainers.containers; import com.github.dockerjava.api.model.Container; import com.google.common.annotations.VisibleForTesting; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.SystemUtils; import org.testcontainers.containers.output.OutputFrame; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.containers.wait.strategy.WaitStrategy; import org.testcontainers.lifecycle.Startable; import org.testcontainers.utility.Base58; import org.testcontainers.utility.DockerImageName; import java.io.File; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; /** * Testcontainers implementation for Docker Compose V2.
* It uses either Compose V2 contained within the Docker binary, or a containerised version of Compose V2. */ @Slf4j public class ComposeContainer implements Startable { private final Map scalingPreferences = new HashMap<>(); private boolean localCompose; private boolean pull = true; private boolean build = false; private Set options = new HashSet<>(); private boolean tailChildContainers; private static final Object MUTEX = new Object(); private List services = new ArrayList<>(); /** * Properties that should be passed through to all Compose and ambassador containers (not * necessarily to containers that are spawned by Compose itself) */ private Map env = new HashMap<>(); private RemoveImages removeImages; private boolean removeVolumes = true; public static final String COMPOSE_EXECUTABLE = SystemUtils.IS_OS_WINDOWS ? "docker.exe" : "docker"; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("docker"); private final ComposeDelegate composeDelegate; private String project; private List filesInDirectory = new ArrayList<>(); /** * Creates a new ComposeContainer using the specified Docker image and compose files. * * @param image The Docker image to use for the container * @param composeFiles One or more Docker Compose configuration files */ public ComposeContainer(DockerImageName image, File... composeFiles) { this(image, Arrays.asList(composeFiles)); } /** * Creates a new ComposeContainer using the specified Docker image and compose files. * * @param image The Docker image to use for the container * @param composeFiles A list of Docker Compose configuration files */ public ComposeContainer(DockerImageName image, List composeFiles) { this(image, Base58.randomString(6).toLowerCase(), composeFiles); } /** * Creates a new ComposeContainer with the specified Docker image, identifier, and compose files. * * @param image The Docker image to use for the container * @param identifier A unique identifier for this compose environment * @param composeFiles One or more Docker Compose configuration files */ public ComposeContainer(DockerImageName image, String identifier, File... composeFiles) { this(image, identifier, Arrays.asList(composeFiles)); } /** * Creates a new ComposeContainer with the specified Docker image, identifier, and a single compose file. * * @param image The Docker image to use for the container * @param identifier A unique identifier for this compose environment * @param composeFile A Docker Compose configuration file */ public ComposeContainer(DockerImageName image, String identifier, File composeFile) { this(image, identifier, Collections.singletonList(composeFile)); } /** * Creates a new ComposeContainer with the specified Docker image, identifier, and compose files. * * @param image The Docker image to use for the container * @param identifier A unique identifier for this compose environment * @param composeFiles A list of Docker Compose configuration files */ public ComposeContainer(DockerImageName image, String identifier, List composeFiles) { image.assertCompatibleWith(DEFAULT_IMAGE_NAME); this.composeDelegate = new ComposeDelegate(ComposeDelegate.ComposeVersion.V2, composeFiles, identifier, COMPOSE_EXECUTABLE, image); this.project = this.composeDelegate.getProject(); } /** * Use the new constructor {@link #ComposeContainer(DockerImageName image, File... composeFiles)} */ public ComposeContainer(File... composeFiles) { this(DEFAULT_IMAGE_NAME, Arrays.asList(composeFiles)); this.localCompose = true; } /** * Use the new constructor {@link #ComposeContainer(DockerImageName image, List composeFiles)} */ public ComposeContainer(List composeFiles) { this(DEFAULT_IMAGE_NAME, composeFiles); this.localCompose = true; } /** * Use the new constructor {@link #ComposeContainer(DockerImageName image, String identifier, File... composeFile)} */ public ComposeContainer(String identifier, File... composeFiles) { this(DEFAULT_IMAGE_NAME, identifier, Arrays.asList(composeFiles)); this.localCompose = true; } /** * Use the new constructor {@link #ComposeContainer(DockerImageName image, String identifier, List composeFiles)} */ public ComposeContainer(String identifier, List composeFiles) { this(DEFAULT_IMAGE_NAME, identifier, composeFiles); this.localCompose = true; } @Override public void start() { synchronized (MUTEX) { this.composeDelegate.registerContainersForShutdown(); if (pull) { try { this.composeDelegate.pullImages(); } catch (ContainerLaunchException e) { log.warn("Exception while pulling images, using local images if available", e); } } this.composeDelegate.createServices( this.localCompose, this.build, this.options, this.services, this.scalingPreferences, this.env, this.filesInDirectory ); this.composeDelegate.startAmbassadorContainer(); this.composeDelegate.waitUntilServiceStarted(this.tailChildContainers); } } @VisibleForTesting List listChildContainers() { return this.composeDelegate.listChildContainers(); } public ComposeContainer withServices(@NonNull String... services) { this.services = Arrays.asList(services); return this; } @Override public void stop() { synchronized (MUTEX) { try { this.composeDelegate.getAmbassadorContainer().stop(); // Kill the services using docker String cmd = ComposeCommand.getDownCommand(ComposeDelegate.ComposeVersion.V2, this.options); if (removeVolumes) { cmd += " -v"; } if (removeImages != null) { cmd += " --rmi " + removeImages.dockerRemoveImagesType(); } this.composeDelegate.runWithCompose(this.localCompose, cmd, this.env, this.filesInDirectory); } finally { this.composeDelegate.clear(); this.project = this.composeDelegate.randomProjectId(); } } } public ComposeContainer withExposedService(String serviceName, int servicePort) { this.composeDelegate.withExposedService(serviceName, servicePort, Wait.defaultWaitStrategy()); return this; } public ComposeContainer withExposedService(String serviceName, int instance, int servicePort) { return withExposedService(serviceName + "-" + instance, servicePort); } public ComposeContainer withExposedService( String serviceName, int instance, int servicePort, WaitStrategy waitStrategy ) { this.composeDelegate.withExposedService(serviceName + "-" + instance, servicePort, waitStrategy); return this; } public ComposeContainer withExposedService( String serviceName, int servicePort, @NonNull WaitStrategy waitStrategy ) { this.composeDelegate.withExposedService(serviceName, servicePort, waitStrategy); return this; } /** * Specify the {@link WaitStrategy} to use to determine if the container is ready. * * @param serviceName the name of the service to wait for * @param waitStrategy the WaitStrategy to use * @return this * @see org.testcontainers.containers.wait.strategy.Wait#defaultWaitStrategy() */ public ComposeContainer waitingFor(String serviceName, @NonNull WaitStrategy waitStrategy) { String serviceInstanceName = this.composeDelegate.getServiceInstanceName(serviceName); this.composeDelegate.addWaitStrategy(serviceInstanceName, waitStrategy); return this; } /** * Get the host (e.g. IP address or hostname) that an exposed service can be found at, from the host machine * (i.e. should be the machine that's running this Java process). *

* The service must have been declared using ComposeContainer#withExposedService. * * @param serviceName the name of the service as set in the docker-compose.yml file. * @param servicePort the port exposed by the service container. * @return a host IP address or hostname that can be used for accessing the service container. */ public String getServiceHost(String serviceName, Integer servicePort) { return this.composeDelegate.getServiceHost(); } /** * Get the port that an exposed service can be found at, from the host machine * (i.e. should be the machine that's running this Java process). *

* The service must have been declared using ComposeContainer#withExposedService. * * @param serviceName the name of the service as set in the docker-compose.yml file. * @param servicePort the port exposed by the service container. * @return a port that can be used for accessing the service container. */ public Integer getServicePort(String serviceName, Integer servicePort) { return this.composeDelegate.getServicePort(serviceName, servicePort); } public ComposeContainer withScaledService(String serviceBaseName, int numInstances) { scalingPreferences.put(serviceBaseName, numInstances); return this; } public ComposeContainer withEnv(String key, String value) { env.put(key, value); return this; } public ComposeContainer withEnv(Map env) { env.forEach(this.env::put); return this; } /** * Whether to pull images first. * * @return this instance, for chaining */ public ComposeContainer withPull(boolean pull) { this.pull = pull; return this; } /** * Whether to tail child container logs. * * @return this instance, for chaining */ public ComposeContainer withTailChildContainers(boolean tailChildContainers) { this.tailChildContainers = tailChildContainers; return this; } /** * Attach an output consumer at container startup, enabling stdout and stderr to be followed, waited on, etc. *

* More than one consumer may be registered. * * @param serviceName the name of the service as set in the docker-compose.yml file * @param consumer consumer that output frames should be sent to * @return this instance, for chaining */ public ComposeContainer withLogConsumer(String serviceName, Consumer consumer) { this.composeDelegate.withLogConsumer(serviceName, consumer); return this; } /** * Whether to always build images before starting containers. * * @return this instance, for chaining */ public ComposeContainer withBuild(boolean build) { this.build = build; return this; } /** * Adds options to the docker command, e.g. docker --compatibility. * * @return this instance, for chaining */ public ComposeContainer withOptions(String... options) { this.options = new HashSet<>(Arrays.asList(options)); return this; } /** * Remove images after containers shutdown. * * @return this instance, for chaining */ public ComposeContainer withRemoveImages(ComposeContainer.RemoveImages removeImages) { this.removeImages = removeImages; return this; } /** * Remove volumes after containers shut down. * * @param removeVolumes whether volumes are to be removed. * @return this instance, for chaining. */ public ComposeContainer withRemoveVolumes(boolean removeVolumes) { this.removeVolumes = removeVolumes; return this; } /** * Set the maximum startup timeout all the waits set are bounded to. * * @return this instance. for chaining */ public ComposeContainer withStartupTimeout(Duration startupTimeout) { this.composeDelegate.setStartupTimeout(startupTimeout); return this; } public ComposeContainer withCopyFilesInContainer(String... fileCopyInclusions) { this.filesInDirectory = Arrays.asList(fileCopyInclusions); return this; } public Optional getContainerByServiceName(String serviceName) { return this.composeDelegate.getContainerByServiceName(serviceName); } private void followLogs(String containerId, Consumer consumer) { this.followLogs(containerId, consumer); } public enum RemoveImages { /** * Remove all images used by any service. */ ALL("all"), /** * Remove only images that don't have a custom tag set by the `image` field. */ LOCAL("local"); private final String dockerRemoveImagesType; RemoveImages(final String dockerRemoveImagesType) { this.dockerRemoveImagesType = dockerRemoveImagesType; } public String dockerRemoveImagesType() { return dockerRemoveImagesType; } } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/ComposeDelegate.java ================================================ package org.testcontainers.containers; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.model.Container; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.Sets; import lombok.Getter; import lombok.NonNull; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.output.OutputFrame; import org.testcontainers.containers.output.Slf4jLogConsumer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.containers.wait.strategy.WaitAllStrategy; import org.testcontainers.containers.wait.strategy.WaitStrategy; import org.testcontainers.images.RemoteDockerImage; import org.testcontainers.utility.Base58; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.ImageNameSubstitutor; import org.testcontainers.utility.LogUtils; import org.testcontainers.utility.ResourceReaper; import java.io.File; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; @Slf4j class ComposeDelegate { private final ComposeVersion composeVersion; private final String composeSeparator; private final DockerClient dockerClient; private final List composeFiles; private final DockerComposeFiles dockerComposeFiles; private final String identifier; @Getter private final String project; private final String executable; private final DockerImageName defaultImageName; private final AtomicInteger nextAmbassadorPort = new AtomicInteger(2000); private final Map> ambassadorPortMappings = new ConcurrentHashMap<>(); private final Map>> logConsumers = new ConcurrentHashMap<>(); @Getter private final SocatContainer ambassadorContainer = new SocatContainer(); private final Map serviceInstanceMap = new ConcurrentHashMap<>(); private final Map waitStrategyMap = new ConcurrentHashMap<>(); @Setter private Duration startupTimeout = Duration.ofMinutes(30); ComposeDelegate( ComposeVersion composeVersion, List composeFiles, String identifier, String executable, DockerImageName defaultImageName ) { this.composeVersion = composeVersion; this.composeSeparator = composeVersion.getSeparator(); this.dockerClient = DockerClientFactory.lazyClient(); this.composeFiles = composeFiles; this.dockerComposeFiles = new DockerComposeFiles(this.composeFiles); this.identifier = identifier.toLowerCase(); this.project = randomProjectId(); this.executable = executable; this.defaultImageName = defaultImageName; } void pullImages() { // Pull images using our docker client rather than compose itself, // (a) as a workaround for https://github.com/docker/compose/issues/5854, which prevents authenticated image pulls being possible when credential helpers are in use // (b) so that credential helper-based auth still works when compose is running from within a container this.dockerComposeFiles.getDependencyImages() .forEach(imageName -> { try { log.info( "Preemptively checking local images for '{}', referenced via a compose file or transitive Dockerfile. If not available, it will be pulled.", imageName ); new RemoteDockerImage(DockerImageName.parse(imageName)) .withImageNameSubstitutor(ImageNameSubstitutor.noop()) .get(); } catch (Exception e) { log.warn( "Unable to pre-fetch an image ({}) depended upon by Docker Compose build - startup will continue but may fail. Exception message was: {}", imageName, e.getMessage() ); } }); } void createServices( boolean localCompose, boolean build, final Set options, final List services, final Map scalingPreferences, Map env, List fileCopyInclusions ) { // services that have been explicitly requested to be started. If empty, all services should be started. final String serviceNameArgs = Stream .concat( services.stream(), // services that have been specified with `withServices` scalingPreferences.keySet().stream() // services that are implicitly needed via `withScaledService` ) .distinct() .collect(Collectors.joining(" ")); // Apply scaling for the services specified using `withScaledService` final String scalingOptions = scalingPreferences .entrySet() .stream() .map(entry -> "--scale " + entry.getKey() + "=" + entry.getValue()) .distinct() .collect(Collectors.joining(" ")); String command = ComposeCommand.getUpCommand(this.composeVersion, options); if (build) { command += " --build"; } if (!Strings.isNullOrEmpty(scalingOptions)) { command += " " + scalingOptions; } if (!Strings.isNullOrEmpty(serviceNameArgs)) { command += " " + serviceNameArgs; } // Run the docker compose container, which starts up the services runWithCompose(localCompose, command, env, fileCopyInclusions); } void waitUntilServiceStarted(boolean tailChildContainers) { listChildContainers().forEach(container -> createServiceInstance(container, tailChildContainers)); Set servicesToWaitFor = waitStrategyMap.keySet(); Set instantiatedServices = serviceInstanceMap.keySet(); Sets.SetView missingServiceInstances = Sets.difference(servicesToWaitFor, instantiatedServices); if (!missingServiceInstances.isEmpty()) { throw new IllegalStateException( "Services named " + missingServiceInstances + " " + "do not exist, but wait conditions have been defined " + "for them. This might mean that you misspelled " + "the service name when defining the wait condition." ); } serviceInstanceMap.forEach(this::waitUntilServiceStarted); } private void createServiceInstance(Container container, boolean tailChildContainers) { String serviceName = getServiceNameFromContainer(container); final ComposeServiceWaitStrategyTarget containerInstance = new ComposeServiceWaitStrategyTarget( dockerClient, container, ambassadorContainer, ambassadorPortMappings.getOrDefault(serviceName, new HashMap<>()) ); String containerId = containerInstance.getContainerId(); if (tailChildContainers) { followLogs(containerId, new Slf4jLogConsumer(log).withPrefix(container.getNames()[0])); } //follow logs using registered consumers for this service logConsumers .getOrDefault(serviceName, Collections.emptyList()) .forEach(consumer -> followLogs(containerId, consumer)); serviceInstanceMap.putIfAbsent(serviceName, containerInstance); } private void waitUntilServiceStarted(String serviceName, ComposeServiceWaitStrategyTarget serviceInstance) { final WaitAllStrategy waitAllStrategy = waitStrategyMap.get(serviceName); if (waitAllStrategy != null) { waitAllStrategy.waitUntilReady(serviceInstance); } } private String getServiceNameFromContainer(com.github.dockerjava.api.model.Container container) { final String containerName = container.getLabels().get("com.docker.compose.service"); final String containerNumber = container.getLabels().get("com.docker.compose.container-number"); return String.format("%s%s%s", containerName, this.composeSeparator, containerNumber); } public void runWithCompose(boolean localCompose, String cmd) { runWithCompose(localCompose, cmd, Collections.emptyMap(), Collections.emptyList()); } public void runWithCompose( boolean localCompose, String cmd, Map env, List fileCopyInclusions ) { Preconditions.checkNotNull(composeFiles); Preconditions.checkArgument(!composeFiles.isEmpty(), "No docker compose file have been provided"); final DockerCompose dockerCompose; if (localCompose) { dockerCompose = new LocalDockerCompose(this.executable, composeFiles, project); } else { dockerCompose = new ContainerisedDockerCompose(this.defaultImageName, composeFiles, project, fileCopyInclusions); } dockerCompose.withCommand(cmd).withEnv(env).invoke(); } void registerContainersForShutdown() { ResourceReaper .instance() .registerLabelsFilterForCleanup(Collections.singletonMap("com.docker.compose.project", project)); } @VisibleForTesting List listChildContainers() { return dockerClient .listContainersCmd() .withShowAll(true) .exec() .stream() .filter(container -> Arrays.stream(container.getNames()).anyMatch(name -> name.startsWith("/" + project))) .collect(Collectors.toList()); } void startAmbassadorContainer() { if (!this.ambassadorPortMappings.isEmpty()) { this.ambassadorContainer.start(); } } public void withExposedService(String serviceName, int servicePort) { withExposedService(serviceName, servicePort, Wait.defaultWaitStrategy()); } public void withExposedService(String serviceName, int instance, int servicePort) { withExposedService(serviceName + this.composeSeparator + instance, servicePort); } public void withExposedService(String serviceName, int instance, int servicePort, WaitStrategy waitStrategy) { withExposedService(serviceName + this.composeSeparator + instance, servicePort, waitStrategy); } public void withExposedService(String serviceName, int servicePort, @NonNull WaitStrategy waitStrategy) { String serviceInstanceName = getServiceInstanceName(serviceName); /* * For every service/port pair that needs to be exposed, we register a target on an 'ambassador container'. * * The ambassador container's role is to link (within the Docker network) to one of the * compose services, and proxy TCP network I/O out to a port that the ambassador container * exposes. * * This avoids the need for the docker compose file to explicitly expose ports on all the * services. * * {@link GenericContainer} should ensure that the ambassador container is on the same network * as the rest of the compose environment. */ // Ambassador container will be started together after docker compose has started int ambassadorPort = nextAmbassadorPort.getAndIncrement(); ambassadorPortMappings .computeIfAbsent(serviceInstanceName, __ -> new ConcurrentHashMap<>()) .put(servicePort, ambassadorPort); ambassadorContainer.withTarget(ambassadorPort, serviceInstanceName, servicePort); ambassadorContainer.addLink( new FutureContainer(this.project + this.composeSeparator + serviceInstanceName), serviceInstanceName ); addWaitStrategy(serviceInstanceName, waitStrategy); } String getServiceInstanceName(String serviceName) { String serviceInstanceName = serviceName; String regex = String.format(".*%s[0-9]+", this.composeSeparator); if (!serviceInstanceName.matches(regex)) { serviceInstanceName += String.format("%s1", this.composeSeparator); // implicit first instance of this service } return serviceInstanceName; } /* * can have multiple wait strategies for a single container, e.g. if waiting on several ports * if no wait strategy is defined, the WaitAllStrategy will return immediately. * The WaitAllStrategy uses the startup timeout for everything as a global maximum, but we expect timeouts to be handled by the inner strategies. */ void addWaitStrategy(String serviceInstanceName, @NonNull WaitStrategy waitStrategy) { final WaitAllStrategy waitAllStrategy = waitStrategyMap.computeIfAbsent( serviceInstanceName, __ -> { return new WaitAllStrategy(WaitAllStrategy.Mode.WITH_MAXIMUM_OUTER_TIMEOUT) .withStartupTimeout(startupTimeout); } ); waitAllStrategy.withStrategy(waitStrategy); } /** * Get the port that an exposed service can be found at, from the host machine * (i.e. should be the machine that's running this Java process). *

* The service must have been declared using DockerComposeContainer#withExposedService. * * @param serviceName the name of the service as set in the docker-compose.yml file. * @param servicePort the port exposed by the service container. * @return a port that can be used for accessing the service container. */ public Integer getServicePort(String serviceName, Integer servicePort) { Map portMap = this.ambassadorPortMappings.get(getServiceInstanceName(serviceName)); if (portMap == null) { throw new IllegalArgumentException( "Could not get a port for '" + serviceName + "'. " + "Testcontainers does not have an exposed port configured for '" + serviceName + "'. " + "To fix, please ensure that the service '" + serviceName + "' has ports exposed using .withExposedService(...)" ); } else { return ambassadorContainer.getMappedPort(portMap.get(servicePort)); } } Optional getContainerByServiceName(String serviceName) { String serviceInstantName = getServiceInstanceName(serviceName); return Optional.ofNullable(serviceInstanceMap.get(serviceInstantName)); } private void followLogs(String containerId, Consumer consumer) { LogUtils.followOutput(dockerClient, containerId, consumer); } String randomProjectId() { return this.identifier + Base58.randomString(6).toLowerCase(); } void withLogConsumer(String serviceName, Consumer consumer) { String serviceInstanceName = getServiceInstanceName(serviceName); final List> consumers = this.logConsumers.getOrDefault(serviceInstanceName, new ArrayList<>()); consumers.add(consumer); this.logConsumers.putIfAbsent(serviceInstanceName, consumers); } String getServiceHost() { return this.ambassadorContainer.getHost(); } void clear() { this.logConsumers.clear(); this.ambassadorPortMappings.clear(); this.serviceInstanceMap.clear(); this.waitStrategyMap.clear(); } enum ComposeVersion { V1("_"), V2("-"); private final String separator; ComposeVersion(String separator) { this.separator = separator; } public String getSeparator() { return this.separator; } } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/ComposeServiceWaitStrategyTarget.java ================================================ package org.testcontainers.containers; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.api.model.Container; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NonNull; import org.testcontainers.containers.wait.strategy.WaitStrategyTarget; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Class to provide a wait strategy target for services started through docker-compose */ @EqualsAndHashCode class ComposeServiceWaitStrategyTarget implements WaitStrategyTarget { private final Container container; private final GenericContainer proxyContainer; private final DockerClient dockerClient; @NonNull private Map mappedPorts; @Getter(lazy = true) private final InspectContainerResponse containerInfo = dockerClient.inspectContainerCmd(getContainerId()).exec(); ComposeServiceWaitStrategyTarget( DockerClient dockerClient, Container container, GenericContainer proxyContainer, @NonNull Map mappedPorts ) { this.dockerClient = dockerClient; this.container = container; this.proxyContainer = proxyContainer; this.mappedPorts = new HashMap<>(mappedPorts); } /** * {@inheritDoc} */ @Override public List getExposedPorts() { return new ArrayList<>(this.mappedPorts.keySet()); } /** * {@inheritDoc} */ @Override public Integer getMappedPort(int originalPort) { return this.proxyContainer.getMappedPort(this.mappedPorts.get(originalPort)); } /** * {@inheritDoc} */ @Override public String getHost() { return proxyContainer.getHost(); } /** * {@inheritDoc} */ @Override public String getContainerId() { return this.container.getId(); } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/Container.java ================================================ package org.testcontainers.containers; import com.github.dockerjava.api.model.Bind; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.NonNull; import lombok.Value; import org.testcontainers.containers.output.OutputFrame; import org.testcontainers.containers.startupcheck.StartupCheckStrategy; import org.testcontainers.containers.traits.LinkableContainer; import org.testcontainers.containers.wait.strategy.WaitStrategy; import org.testcontainers.images.ImagePullPolicy; import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.LogUtils; import org.testcontainers.utility.MountableFile; import java.time.Duration; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.Future; import java.util.function.Consumer; import java.util.function.Function; public interface Container> extends LinkableContainer, ContainerState { /** * @return a reference to this container instance, cast to the expected generic type. */ @SuppressWarnings("unchecked") default SELF self() { return (SELF) this; } /** * Class to hold results from a "docker exec" command */ @Value @AllArgsConstructor(access = AccessLevel.MODULE) class ExecResult { int exitCode; String stdout; String stderr; } /** * Set the command that should be run in the container. Consider using {@link #withCommand(String)} * for building a container in a fluent style. * * @param command a command in single string format (will automatically be split on spaces) */ void setCommand(@NonNull String command); /** * Set the command that should be run in the container. Consider using {@link #withCommand(String...)} * for building a container in a fluent style. * * @param commandParts a command as an array of string parts */ void setCommand(@NonNull String... commandParts); /** * Add an environment variable to be passed to the container. Consider using {@link #withEnv(String, String)} * for building a container in a fluent style. * * @param key environment variable key * @param value environment variable value */ void addEnv(String key, String value); /** * Adds a file system binding. Consider using {@link #withFileSystemBind(String, String, BindMode)} * for building a container in a fluent style. * * @param hostPath the file system path on the host * @param containerPath the file system path inside the container * @param mode the bind mode * @deprecated use {@link GenericContainer#withCopyToContainer(Transferable, String)} */ @Deprecated default void addFileSystemBind(final String hostPath, final String containerPath, final BindMode mode) { addFileSystemBind(hostPath, containerPath, mode, SelinuxContext.SHARED); } /** * Adds a file system binding. Consider using {@link #withFileSystemBind(String, String, BindMode)} * for building a container in a fluent style. * * @param hostPath the file system path on the host * @param containerPath the file system path inside the container * @param mode the bind mode * @param selinuxContext selinux context argument to use for this file * @deprecated use {@link GenericContainer#withCopyToContainer(Transferable, String)} */ @Deprecated void addFileSystemBind(String hostPath, String containerPath, BindMode mode, SelinuxContext selinuxContext); /** * Add a link to another container. * * @param otherContainer the other container object to link to * @param alias the alias (for the other container) that this container should be able to use * @deprecated Links are deprecated (see #465). Please use {@link Network} features instead. */ @Deprecated void addLink(LinkableContainer otherContainer, String alias); /** * Add an exposed port. Consider using {@link #withExposedPorts(Integer...)} * for building a container in a fluent style. * * @param port a TCP port */ void addExposedPort(Integer port); /** * Add exposed ports. Consider using {@link #withExposedPorts(Integer...)} * for building a container in a fluent style. * * @param ports an array of TCP ports */ void addExposedPorts(int... ports); /** * Specify the {@link WaitStrategy} to use to determine if the container is ready. * * @see org.testcontainers.containers.wait.strategy.Wait#defaultWaitStrategy() * @param waitStrategy the WaitStrategy to use * @return this */ SELF waitingFor(@NonNull WaitStrategy waitStrategy); /** * Adds a file system binding. * * @param hostPath the file system path on the host * @param containerPath the file system path inside the container * @return this * @deprecated use {@link GenericContainer#withCopyToContainer(Transferable, String)} */ @Deprecated default SELF withFileSystemBind(String hostPath, String containerPath) { return withFileSystemBind(hostPath, containerPath, BindMode.READ_WRITE); } /** * Adds a file system binding. * * @param hostPath the file system path on the host * @param containerPath the file system path inside the container * @param mode the bind mode * @return this * @deprecated use {@link GenericContainer#withCopyToContainer(Transferable, String)} */ @Deprecated SELF withFileSystemBind(String hostPath, String containerPath, BindMode mode); /** * Adds container volumes. * * @param container the container to add volumes from * @param mode the bind mode * @return this */ SELF withVolumesFrom(Container container, BindMode mode); /** * Set the ports that this container listens on * * @param ports an array of TCP ports * @return this */ SELF withExposedPorts(Integer... ports); /** * Set the file to be copied before starting a created container * * @param mountableFile a Mountable file with path of source file / folder on host machine * @param containerPath a destination path on container to which the files / folders to be copied * @return this * * @deprecated Use {@link #withCopyToContainer(Transferable, String)} instead */ @Deprecated SELF withCopyFileToContainer(MountableFile mountableFile, String containerPath); /** * Set the content to be copied before starting a created container * * @param transferable a Transferable * @param containerPath a destination path on container to which the files / folders to be copied * @return this */ SELF withCopyToContainer(Transferable transferable, String containerPath); /** * Add an environment variable to be passed to the container. * * @param key environment variable key * @param value environment variable value * @return this */ SELF withEnv(String key, String value); /** * Add an environment variable to be passed to the container. * * @param key environment variable key * @param mapper environment variable value mapper, accepts old value as an argument * @return this */ default SELF withEnv(String key, Function, String> mapper) { Optional oldValue = Optional.ofNullable(getEnvMap().get(key)); return withEnv(key, mapper.apply(oldValue)); } /** * Add environment variables to be passed to the container. * * @param env map of environment variables * @return this */ SELF withEnv(Map env); /** * Add a label to the container. * * @param key label key * @param value label value * @return this */ SELF withLabel(String key, String value); /** * Add labels to the container. * @param labels map of labels * @return this */ SELF withLabels(Map labels); /** * Set the command that should be run in the container * * @param cmd a command in single string format (will automatically be split on spaces) * @return this */ SELF withCommand(String cmd); /** * Set the command that should be run in the container * * @param commandParts a command as an array of string parts * @return this */ SELF withCommand(String... commandParts); /** * Add an extra host entry to be passed to the container * @param hostname hostname to use for this hosts file entry * @param ipAddress IP address to use for this hosts file entry * @return this */ SELF withExtraHost(String hostname, String ipAddress); /** * Set the network mode for this container, similar to the --net <name> * option on the docker CLI. * * @param networkMode network mode, e.g. including 'host', 'bridge', 'none' or the name of an existing named network. * @return this */ SELF withNetworkMode(String networkMode); /** * Set the network for this container, similar to the --network <name> * option on the docker CLI. * * @param network the instance of {@link Network} * @return this */ SELF withNetwork(Network network); /** * Set the network aliases for this container, similar to the --network-alias <my-service> * option on the docker CLI. * * @param aliases the list of aliases * @return this */ SELF withNetworkAliases(String... aliases); /** * Set the image pull policy of the container * @return */ SELF withImagePullPolicy(ImagePullPolicy policy); /** * Map a resource (file or directory) on the classpath to a path inside the container. * This will only work if you are running your tests outside a Docker container. * * @param resourcePath path to the resource on the classpath (relative to the classpath root; should not start with a leading slash) * @param containerPath path this should be mapped to inside the container * @param mode access mode for the file * @return this * @deprecated use {@link GenericContainer#withCopyToContainer(Transferable, String)} */ @Deprecated default SELF withClasspathResourceMapping( final String resourcePath, final String containerPath, final BindMode mode ) { withClasspathResourceMapping(resourcePath, containerPath, mode, SelinuxContext.SHARED); return self(); } /** * Map a resource (file or directory) on the classpath to a path inside the container. * This will only work if you are running your tests outside a Docker container. * * @param resourcePath path to the resource on the classpath (relative to the classpath root; should not start with a leading slash) * @param containerPath path this should be mapped to inside the container * @param mode access mode for the file * @param selinuxContext selinux context argument to use for this file * @return this * @deprecated use {@link GenericContainer#withCopyToContainer(Transferable, String)} */ @Deprecated SELF withClasspathResourceMapping( String resourcePath, String containerPath, BindMode mode, SelinuxContext selinuxContext ); /** * Set the duration of waiting time until container treated as started. * @see WaitStrategy#waitUntilReady(org.testcontainers.containers.wait.strategy.WaitStrategyTarget) * * @param startupTimeout timeout * @return this */ SELF withStartupTimeout(Duration startupTimeout); /** * Set the privilegedMode mode for the container * @param mode boolean * @return this */ SELF withPrivilegedMode(boolean mode); /** * Only consider a container to have successfully started if it has been running for this duration. The default * value is null; if that's the value, ignore this check. * * @param minimumRunningDuration duration this container should run for if started successfully * @return this */ SELF withMinimumRunningDuration(Duration minimumRunningDuration); /** * Set the startup check strategy used for checking whether the container has started. * * @param strategy startup check strategy */ SELF withStartupCheckStrategy(StartupCheckStrategy strategy); /** * Set the working directory that the container should use on startup. * * @param workDir path to the working directory inside the container */ SELF withWorkingDirectory(String workDir); /** * Resolve Docker image and set it. * * @param dockerImageName image name */ void setDockerImageName(@NonNull String dockerImageName); /** * Get image name. * * @return image name */ @NonNull String getDockerImageName(); /** * Get the IP address that containers (e.g. browsers) can use to reference a service running on the local machine, * i.e. the machine on which this test is running. *

* For example, if a web server is running on port 8080 on this local machine, the containerized web driver needs * to be pointed at "http://" + getTestHostIpAddress() + ":8080" in order to access it. Trying to hit localhost * from inside the container is not going to work, since the container has its own IP address. * * @return the IP address of the host machine * @deprecated use {@link org.testcontainers.Testcontainers#exposeHostPorts(int...)} */ @Deprecated String getTestHostIpAddress(); /** * Follow container output, sending each frame (usually, line) to a consumer. Stdout and stderr will be followed. * * @param consumer consumer that the frames should be sent to */ default void followOutput(Consumer consumer) { LogUtils.followOutput(getDockerClient(), getContainerId(), consumer); } /** * Follow container output, sending each frame (usually, line) to a consumer. This method allows Stdout and/or stderr * to be selected. * * @param consumer consumer that the frames should be sent to * @param types types that should be followed (one or both of STDOUT, STDERR) */ default void followOutput(Consumer consumer, OutputFrame.OutputType... types) { LogUtils.followOutput(getDockerClient(), getContainerId(), consumer, types); } /** * Attach an output consumer at container startup, enabling stdout and stderr to be followed, waited on, etc. *

* More than one consumer may be registered. * * @param consumer consumer that output frames should be sent to * @return this */ SELF withLogConsumer(Consumer consumer); List getPortBindings(); List getExtraHosts(); Future getImage(); /** * * @deprecated use getEnvMap */ @Deprecated List getEnv(); Map getEnvMap(); String[] getCommandParts(); List getBinds(); /** * @deprecated Links are deprecated (see #465). Please use {@link Network} features instead. */ @Deprecated Map getLinkedContainers(); void setExposedPorts(List exposedPorts); void setPortBindings(List portBindings); void setExtraHosts(List extraHosts); void setImage(Future image); void setEnv(List env); void setCommandParts(String[] commandParts); void setBinds(List binds); /** * @deprecated Links are deprecated (see #465). Please use {@link Network} features instead. */ @Deprecated void setLinkedContainers(Map linkedContainers); void setWaitStrategy(WaitStrategy waitStrategy); } ================================================ FILE: core/src/main/java/org/testcontainers/containers/ContainerDef.java ================================================ package org.testcontainers.containers; import com.github.dockerjava.api.command.CreateContainerCmd; import com.github.dockerjava.api.model.Bind; import com.github.dockerjava.api.model.ExposedPort; import com.github.dockerjava.api.model.HostConfig; import com.github.dockerjava.api.model.InternetProtocol; import com.github.dockerjava.api.model.PortBinding; import com.github.dockerjava.api.model.Ports; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.testcontainers.DockerClientFactory; import org.testcontainers.UnstableAPI; import org.testcontainers.containers.wait.strategy.WaitStrategy; import org.testcontainers.images.RemoteDockerImage; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.TestcontainersConfiguration; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @UnstableAPI @Slf4j class ContainerDef { @Getter private RemoteDockerImage image; Set exposedPorts = new LinkedHashSet<>(); Set portBindings = new HashSet<>(); Map labels = new HashMap<>(); Map envVars = new HashMap<>(); private String[] entrypoint; private String[] command = new String[0]; @Getter private Network network; Set networkAliases = new LinkedHashSet<>(); @Getter private String networkMode; List binds = new ArrayList<>(); @Getter private boolean privilegedMode; @Getter private WaitStrategy waitStrategy = GenericContainer.DEFAULT_WAIT_STRATEGY; public ContainerDef() {} protected void applyTo(CreateContainerCmd createCommand) { HostConfig hostConfig = createCommand.getHostConfig(); if (hostConfig == null) { hostConfig = new HostConfig(); createCommand.withHostConfig(hostConfig); } // PortBindings must contain: // * all exposed ports with a randomized host port (equivalent to -p CONTAINER_PORT) // * all exposed ports with a fixed host port (equivalent to -p HOST_PORT:CONTAINER_PORT) Map allPortBindings = new HashMap<>(); // First, collect all the randomized host ports from our 'exposedPorts' field for (ExposedPort exposedPort : this.exposedPorts) { allPortBindings.put(exposedPort, new PortBinding(Ports.Binding.empty(), exposedPort)); } // Next, collect all the fixed host ports from our 'portBindings' field, overwriting any randomized ports so that // we don't create two bindings for the same container port. for (PortBinding portBinding : this.portBindings) { allPortBindings.put(portBinding.getExposedPort(), portBinding); } hostConfig.withPortBindings(new ArrayList<>(allPortBindings.values())); // Next, ExposedPorts must be set up to publish all of the above ports, both randomized and fixed. createCommand.withExposedPorts(new ArrayList<>(allPortBindings.keySet())); createCommand.withEnv( this.envVars.entrySet() .stream() .filter(it -> it.getValue() != null) .map(it -> it.getKey() + "=" + it.getValue()) .toArray(String[]::new) ); if (this.entrypoint != null) { createCommand.withEntrypoint(this.entrypoint); } if (this.command != null) { createCommand.withCmd(this.command); } if (this.network != null) { hostConfig.withNetworkMode(this.network.getId()); createCommand.withAliases(this.networkAliases.toArray(new String[0])); } else { if (this.networkMode != null) { createCommand.getHostConfig().withNetworkMode(this.networkMode); } } boolean shouldCheckFileMountingSupport = this.binds.size() > 0 && !TestcontainersConfiguration.getInstance().isDisableChecks(); if (shouldCheckFileMountingSupport) { if (!DockerClientFactory.instance().isFileMountingSupported()) { log.warn( "Unable to mount a file from test host into a running container. " + "This may be a misconfiguration or limitation of your Docker environment. " + "Some features might not work." ); } } hostConfig.withBinds(this.binds.toArray(new Bind[0])); if (this.privilegedMode) { createCommand.getHostConfig().withPrivileged(this.privilegedMode); } Map combinedLabels = new HashMap<>(this.labels); if (createCommand.getLabels() != null) { combinedLabels.putAll(createCommand.getLabels()); } createCommand.withLabels(combinedLabels); } protected void setImage(RemoteDockerImage image) { this.image = image; } protected void setImage(String image) { setImage(DockerImageName.parse(image)); } protected void setImage(DockerImageName image) { setImage(new RemoteDockerImage(image)); } public Set getExposedPorts() { return new LinkedHashSet<>(this.exposedPorts); } protected void setExposedPorts(Set exposedPorts) { this.exposedPorts.clear(); this.exposedPorts.addAll(exposedPorts); } protected void addExposedPorts(ExposedPort... exposedPorts) { this.exposedPorts.addAll(Arrays.asList(exposedPorts)); } protected void addExposedPort(ExposedPort exposedPort) { this.exposedPorts.add(exposedPort); } protected void setExposedTcpPorts(Set ports) { this.exposedPorts.clear(); ports.forEach(port -> this.exposedPorts.add(ExposedPort.tcp(port))); } protected void addExposedTcpPorts(int... ports) { for (int port : ports) { this.exposedPorts.add(ExposedPort.tcp(port)); } } protected void addExposedTcpPort(int port) { this.exposedPorts.add(ExposedPort.tcp(port)); } protected void addExposedPort(int port, InternetProtocol protocol) { this.exposedPorts.add(new ExposedPort(port, protocol)); } public Set getPortBindings() { return new HashSet<>(this.portBindings); } protected void setPortBindings(Set portBindings) { this.portBindings.clear(); this.portBindings.addAll(portBindings); } protected void addPortBindings(PortBinding... portBindings) { this.portBindings.addAll(Arrays.asList(portBindings)); } protected void addPortBinding(PortBinding portBinding) { this.portBindings.add(portBinding); } public Map getLabels() { return new HashMap<>(this.labels); } protected void setLabels(Map labels) { this.labels.clear(); this.labels.putAll(labels); } protected void addLabels(Map labels) { this.labels.putAll(labels); } protected void addLabel(String key, String value) { this.labels.put(key, value); } public Map getEnvVars() { return new HashMap<>(this.envVars); } protected void setEnvVars(Map envVars) { this.envVars.clear(); this.envVars.putAll(envVars); } protected void addEnvVars(Map envVars) { this.envVars.putAll(envVars); } protected void addEnvVar(String key, String value) { this.envVars.put(key, value); } public String[] getEntrypoint() { return Arrays.copyOf(this.entrypoint, this.entrypoint.length); } protected void setEntrypoint(String... entrypoint) { this.entrypoint = entrypoint; } public String[] getCommand() { return Arrays.copyOf(this.command, this.command.length); } protected void setCommand(String... command) { this.command = command; } protected void setNetwork(Network network) { this.network = network; } public Set getNetworkAliases() { return new LinkedHashSet<>(this.networkAliases); } protected void setNetworkAliases(Set aliases) { this.networkAliases.clear(); this.networkAliases.addAll(aliases); } protected void addNetworkAliases(String... aliases) { this.networkAliases.addAll(Arrays.asList(aliases)); } protected void addNetworkAlias(String alias) { this.networkAliases.add(alias); } protected void setNetworkMode(String networkMode) { this.networkMode = networkMode; } protected void setPrivilegedMode(boolean privilegedMode) { this.privilegedMode = privilegedMode; } public List getBinds() { return new ArrayList<>(this.binds); } protected void setBinds(List binds) { this.binds.clear(); this.binds.addAll(binds); } protected void addBinds(Bind... binds) { this.binds.addAll(Arrays.asList(binds)); } protected void addBind(Bind bind) { this.binds.add(bind); } protected void setWaitStrategy(WaitStrategy waitStrategy) { this.waitStrategy = waitStrategy; } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/ContainerFetchException.java ================================================ package org.testcontainers.containers; /** * Created by rnorth on 15/10/2015. */ public class ContainerFetchException extends RuntimeException { public ContainerFetchException(String s, Exception e) { super(s, e); } public ContainerFetchException(String s) { super(s); } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/ContainerLaunchException.java ================================================ package org.testcontainers.containers; /** * AN exception that may be raised during launch of a container. */ public class ContainerLaunchException extends RuntimeException { public ContainerLaunchException(String message) { super(message); } public ContainerLaunchException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/ContainerState.java ================================================ package org.testcontainers.containers; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.HealthState; import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.api.exception.DockerException; import com.github.dockerjava.api.model.ExposedPort; import com.github.dockerjava.api.model.PortBinding; import com.github.dockerjava.api.model.Ports; import com.google.common.base.Preconditions; import lombok.SneakyThrows; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.apache.commons.compress.utils.IOUtils; import org.apache.commons.lang3.math.NumberUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.output.OutputFrame; import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.LogUtils; import org.testcontainers.utility.MountableFile; import org.testcontainers.utility.ThrowingFunction; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; public interface ContainerState { String STATE_HEALTHY = "healthy"; /** * Get the IP address that this container may be reached on (may not be the local machine). * * @return an IP address * @deprecated use {@link #getHost()} * @see #getHost() */ @Deprecated default String getContainerIpAddress() { return getHost(); } default DockerClient getDockerClient() { return DockerClientFactory.lazyClient(); } /** * Get the host that this container may be reached on (may not be the local machine). * * @return a host */ default String getHost() { return DockerClientFactory.instance().dockerHostIpAddress(); } /** * @return is the container currently running? */ default boolean isRunning() { if (getContainerId() == null) { return false; } try { Boolean running = getCurrentContainerInfo().getState().getRunning(); return Boolean.TRUE.equals(running); } catch (DockerException e) { return false; } } /** * @return is the container created? */ default boolean isCreated() { if (getContainerId() == null) { return false; } try { String status = getCurrentContainerInfo().getState().getStatus(); return "created".equalsIgnoreCase(status) || isRunning(); } catch (DockerException e) { return false; } } /** * @return has the container health state 'healthy'? */ default boolean isHealthy() { if (getContainerId() == null) { return false; } try { InspectContainerResponse inspectContainerResponse = getCurrentContainerInfo(); HealthState health = inspectContainerResponse.getState().getHealth(); if (health == null) { throw new RuntimeException( "This container's image does not have a healthcheck declared, so health cannot be determined. Either amend the image or use another approach to determine whether containers are healthy." ); } return STATE_HEALTHY.equals(health.getStatus()); } catch (DockerException e) { return false; } } /** * Inspects the container and returns up-to-date inspection response. * * @return up-to-date container inspect response * @see #getContainerInfo() */ default InspectContainerResponse getCurrentContainerInfo() { return getDockerClient().inspectContainerCmd(getContainerId()).exec(); } /** * Get the actual mapped port for a first port exposed by the container. * Should be used in conjunction with {@link #getHost()}. * * @return the port that the exposed port is mapped to * @throws IllegalStateException if there are no exposed ports */ default Integer getFirstMappedPort() { return getExposedPorts() .stream() .findFirst() .map(this::getMappedPort) .orElseThrow(() -> new IllegalStateException("Container doesn't expose any ports")); } /** * Get the actual mapped port for a given port exposed by the container. * It should be used in conjunction with {@link #getHost()}. *

* Note: The returned port number might be outdated (for instance, after disconnecting from a network and reconnecting * again). If you always need up-to-date value, override the {@link #getContainerInfo()} to return the * {@link #getCurrentContainerInfo()}. * * @param originalPort the original TCP port that is exposed * @return the port that the exposed port is mapped to, or null if it is not exposed * @see #getContainerInfo() * @see #getCurrentContainerInfo() */ default Integer getMappedPort(int originalPort) { Preconditions.checkState( this.getContainerId() != null, "Mapped port can only be obtained after the container is started" ); Ports.Binding[] binding = new Ports.Binding[0]; final InspectContainerResponse containerInfo = this.getContainerInfo(); if (containerInfo != null) { binding = containerInfo.getNetworkSettings().getPorts().getBindings().get(new ExposedPort(originalPort)); } if (binding != null && binding.length > 0 && binding[0] != null) { return Integer.valueOf(binding[0].getHostPortSpec()); } else { throw new IllegalArgumentException("Requested port (" + originalPort + ") is not mapped"); } } /** * @return the exposed ports */ List getExposedPorts(); /** * @return the port bindings */ default List getPortBindings() { List portBindings = new ArrayList<>(); final Ports hostPortBindings = this.getContainerInfo().getHostConfig().getPortBindings(); for (Map.Entry binding : hostPortBindings.getBindings().entrySet()) { for (Ports.Binding portBinding : binding.getValue()) { portBindings.add(String.format("%s:%s", portBinding.toString(), binding.getKey())); } } return portBindings; } /** * @return the bound port numbers */ default List getBoundPortNumbers() { return getPortBindings() .stream() .map(PortBinding::parse) .map(PortBinding::getBinding) .map(Ports.Binding::getHostPortSpec) .filter(Objects::nonNull) .filter(NumberUtils::isNumber) .map(Integer::valueOf) .filter(port -> port > 0) .collect(Collectors.toList()); } /** * @return all log output from the container from start until the current instant (both stdout and stderr) */ default String getLogs() { return LogUtils.getOutput(getDockerClient(), getContainerId()); } /** * @param types log types to return * @return all log output from the container from start until the current instant */ default String getLogs(OutputFrame.OutputType... types) { return LogUtils.getOutput(getDockerClient(), getContainerId(), types); } /** * @return the id of the container */ default String getContainerId() { return getContainerInfo().getId(); } /** * Returns the container inspect response. The response might be cached/outdated. * * @return the container info * @see #getCurrentContainerInfo() */ InspectContainerResponse getContainerInfo(); /** * Run a command inside a running container, as though using "docker exec", and interpreting * the output as UTF8. *

* @see #execInContainer(Charset, String...) */ default Container.ExecResult execInContainer(String... command) throws UnsupportedOperationException, IOException, InterruptedException { return execInContainer(StandardCharsets.UTF_8, command); } /** * Run a command inside a running container, as though using "docker exec". *

* @see ExecInContainerPattern#execInContainer(DockerClient, InspectContainerResponse, Charset, String...) */ default Container.ExecResult execInContainer(Charset outputCharset, String... command) throws UnsupportedOperationException, IOException, InterruptedException { return ExecInContainerPattern.execInContainer(getDockerClient(), getContainerInfo(), outputCharset, command); } /** * Run a command inside a running container as a given user, as using "docker exec -u user". *

* @see ExecInContainerPattern#execInContainerWithUser(DockerClient, InspectContainerResponse, String, String...) * @deprecated use {@link #execInContainer(ExecConfig)} */ @Deprecated default Container.ExecResult execInContainerWithUser(String user, String... command) throws UnsupportedOperationException, IOException, InterruptedException { return ExecInContainerPattern.execInContainer( getDockerClient(), getContainerInfo(), ExecConfig.builder().user(user).command(command).build() ); } /** * Run a command inside a running container as a given user, as using "docker exec -u user". *

* @see ExecInContainerPattern#execInContainerWithUser(DockerClient, InspectContainerResponse, Charset, String, String...) * @deprecated use {@link #execInContainer(Charset, ExecConfig)} */ @Deprecated default Container.ExecResult execInContainerWithUser(Charset outputCharset, String user, String... command) throws UnsupportedOperationException, IOException, InterruptedException { return ExecInContainerPattern.execInContainer( getDockerClient(), getContainerInfo(), outputCharset, ExecConfig.builder().user(user).command(command).build() ); } /** * Run a command inside a running container, as though using "docker exec". */ default Container.ExecResult execInContainer(ExecConfig execConfig) throws UnsupportedOperationException, IOException, InterruptedException { return ExecInContainerPattern.execInContainer(getDockerClient(), getContainerInfo(), execConfig); } /** * Run a command inside a running container, as though using "docker exec". */ default Container.ExecResult execInContainer(Charset outputCharset, ExecConfig execConfig) throws UnsupportedOperationException, IOException, InterruptedException { return ExecInContainerPattern.execInContainer(getDockerClient(), getContainerInfo(), outputCharset, execConfig); } /** * * Copies a file or directory to the container. * * @param mountableFile file or directory which is copied into the container * @param containerPath destination path inside the container */ default void copyFileToContainer(MountableFile mountableFile, String containerPath) { File sourceFile = new File(mountableFile.getResolvedPath()); if (containerPath.endsWith("/") && sourceFile.isFile()) { final Logger logger = LoggerFactory.getLogger(GenericContainer.class); logger.warn( "folder-like containerPath in copyFileToContainer is deprecated, please explicitly specify a file path" ); copyFileToContainer((Transferable) mountableFile, containerPath + sourceFile.getName()); } else { copyFileToContainer((Transferable) mountableFile, containerPath); } } /** * * Copies a file to the container. * * @param transferable file which is copied into the container * @param containerPath destination path inside the container */ @SneakyThrows({ IOException.class, InterruptedException.class }) default void copyFileToContainer(Transferable transferable, String containerPath) { if (getContainerId() == null) { throw new IllegalStateException("copyFileToContainer can only be used with created / running container"); } try ( PipedOutputStream pipedOutputStream = new PipedOutputStream(); PipedInputStream pipedInputStream = new PipedInputStream(pipedOutputStream); TarArchiveOutputStream tarArchive = new TarArchiveOutputStream(pipedOutputStream) ) { Thread thread = new Thread(() -> { try { tarArchive.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); tarArchive.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX); transferable.transferTo(tarArchive, containerPath); } finally { IOUtils.closeQuietly(tarArchive); } }); thread.start(); getDockerClient() .copyArchiveToContainerCmd(getContainerId()) .withTarInputStream(pipedInputStream) .withRemotePath("/") .exec(); thread.join(); } } /** * Copies a file which resides inside the container to user defined directory * * @param containerPath path to file which is copied from container * @param destinationPath destination path to which file is copied with file name * @throws IOException if there's an issue communicating with Docker or receiving entry from TarArchiveInputStream * @throws InterruptedException if the thread waiting for the response is interrupted */ default void copyFileFromContainer(String containerPath, String destinationPath) throws IOException, InterruptedException { copyFileFromContainer( containerPath, inputStream -> { try (FileOutputStream output = new FileOutputStream(destinationPath)) { IOUtils.copy(inputStream, output); return null; } } ); } /** * Streams a file which resides inside the container * * @param containerPath path to file which is copied from container * @param function function that takes InputStream of the copied file */ @SneakyThrows default T copyFileFromContainer(String containerPath, ThrowingFunction function) { if (getContainerId() == null) { throw new IllegalStateException("copyFileFromContainer can only be used when the Container is created."); } DockerClient dockerClient = getDockerClient(); try ( InputStream inputStream = dockerClient.copyArchiveFromContainerCmd(getContainerId(), containerPath).exec(); TarArchiveInputStream tarInputStream = new TarArchiveInputStream(inputStream) ) { tarInputStream.getNextTarEntry(); return function.apply(tarInputStream); } } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/ContainerisedDockerCompose.java ================================================ package org.testcontainers.containers; import com.google.common.base.Joiner; import com.google.common.util.concurrent.Uninterruptibles; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.output.Slf4jLogConsumer; import org.testcontainers.containers.startupcheck.IndefiniteWaitOneShotStartupCheckStrategy; import org.testcontainers.utility.AuditLogger; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import org.testcontainers.utility.PathUtils; import java.io.File; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** * Use Docker Compose container. */ class ContainerisedDockerCompose extends GenericContainer implements DockerCompose { public static final char UNIX_PATH_SEPARATOR = ':'; public ContainerisedDockerCompose( DockerImageName dockerImageName, List composeFiles, String identifier, List fileCopyInclusions ) { super(dockerImageName); addEnv(ENV_PROJECT_NAME, identifier); // Map the docker compose file into the container final File dockerComposeBaseFile = composeFiles.get(0); final String pwd = dockerComposeBaseFile.getAbsoluteFile().getParentFile().getAbsolutePath(); final String containerPwd = convertToUnixFilesystemPath(pwd); final List absoluteDockerComposeFiles = composeFiles .stream() .map(File::getAbsolutePath) .map(MountableFile::forHostPath) .map(MountableFile::getFilesystemPath) .map(this::convertToUnixFilesystemPath) .collect(Collectors.toList()); final String composeFileEnvVariableValue = Joiner.on(UNIX_PATH_SEPARATOR).join(absoluteDockerComposeFiles); // we always need the UNIX path separator logger().debug("Set env COMPOSE_FILE={}", composeFileEnvVariableValue); addEnv(ENV_COMPOSE_FILE, composeFileEnvVariableValue); if (fileCopyInclusions.isEmpty()) { logger().info("Copying all files in {} into the container", pwd); withCopyFileToContainer(MountableFile.forHostPath(pwd), containerPwd); } else { // Always copy the compose file itself logger().info("Copying docker compose file: {}", dockerComposeBaseFile.getAbsolutePath()); withCopyFileToContainer( MountableFile.forHostPath(dockerComposeBaseFile.getAbsolutePath()), convertToUnixFilesystemPath(dockerComposeBaseFile.getAbsolutePath()) ); for (String pathToCopy : fileCopyInclusions) { String hostPath = pwd + "/" + pathToCopy; logger().info("Copying inclusion file: {}", hostPath); withCopyFileToContainer(MountableFile.forHostPath(hostPath), convertToUnixFilesystemPath(hostPath)); } } // Ensure that compose can access docker. Since the container is assumed to be running on the same machine // as the docker daemon, just mapping the docker control socket is OK. // As there seems to be a problem with mapping to the /var/run directory in certain environments (e.g. CircleCI) // we map the socket file outside of /var/run, as just /docker.sock addFileSystemBind( DockerClientFactory.instance().getRemoteDockerUnixSocketPath(), "/docker.sock", BindMode.READ_WRITE ); addEnv("DOCKER_HOST", "unix:///docker.sock"); setStartupCheckStrategy(new IndefiniteWaitOneShotStartupCheckStrategy()); setWorkingDirectory(containerPwd); } @Override public void invoke() { super.start(); followOutput(new Slf4jLogConsumer(logger())); // wait for the compose container to stop, which should only happen after it has spawned all the service containers logger().info("Docker Compose container is running for command: {}", Joiner.on(" ").join(getCommandParts())); while (isRunning()) { logger().trace("Compose container is still running"); Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS); } AuditLogger.doComposeLog(getCommandParts(), getEnv()); final Integer exitCode = getDockerClient() .inspectContainerCmd(getContainerId()) .exec() .getState() .getExitCode(); if (exitCode == null || exitCode != 0) { throw new ContainerLaunchException( "Containerised Docker Compose exited abnormally with code " + exitCode + " whilst running command: " + StringUtils.join(getCommandParts(), ' ') ); } logger().info("Docker Compose has finished running"); } private String convertToUnixFilesystemPath(String path) { return SystemUtils.IS_OS_WINDOWS ? PathUtils.createMinGWPath(path).substring(1) : path; } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/DockerCompose.java ================================================ package org.testcontainers.containers; import java.util.Map; interface DockerCompose { String ENV_PROJECT_NAME = "COMPOSE_PROJECT_NAME"; String ENV_COMPOSE_FILE = "COMPOSE_FILE"; DockerCompose withCommand(String cmd); DockerCompose withEnv(Map env); void invoke(); } ================================================ FILE: core/src/main/java/org/testcontainers/containers/DockerComposeContainer.java ================================================ package org.testcontainers.containers; import com.github.dockerjava.api.model.Container; import com.google.common.annotations.VisibleForTesting; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.SystemUtils; import org.testcontainers.containers.output.OutputFrame; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.containers.wait.strategy.WaitStrategy; import org.testcontainers.lifecycle.Startable; import org.testcontainers.utility.Base58; import org.testcontainers.utility.DockerImageName; import java.io.File; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; /** * Container which launches Docker Compose, for the purposes of launching a defined set of containers. */ @Slf4j public class DockerComposeContainer> implements Startable { private final Map scalingPreferences = new HashMap<>(); private boolean localCompose; private boolean pull = true; private boolean build = false; private Set options = new HashSet<>(); private boolean tailChildContainers; private static final Object MUTEX = new Object(); private List services = new ArrayList<>(); /** * Properties that should be passed through to all Compose and ambassador containers (not * necessarily to containers that are spawned by Compose itself) */ private Map env = new HashMap<>(); private RemoveImages removeImages; private boolean removeVolumes = true; public static final String COMPOSE_EXECUTABLE = SystemUtils.IS_OS_WINDOWS ? "docker-compose.exe" : "docker-compose"; private final ComposeDelegate composeDelegate; private String project; private List filesInDirectory = new ArrayList<>(); /** * Creates a new DockerComposeContainer using the specified Docker image and compose files. * * @param image The Docker image to use for the container * @param composeFiles One or more Docker Compose configuration files */ public DockerComposeContainer(DockerImageName image, File... composeFiles) { this(image, Arrays.asList(composeFiles)); } /** * Creates a new DockerComposeContainer using the specified Docker image and compose files. * * @param image The Docker image to use for the container * @param composeFiles A list of Docker Compose configuration files */ public DockerComposeContainer(DockerImageName image, List composeFiles) { this(image, Base58.randomString(6).toLowerCase(), composeFiles); } /** * Creates a new DockerComposeContainer with the specified Docker image, identifier, and compose files. * * @param image The Docker image to use for the container * @param identifier A unique identifier for this compose environment * @param composeFiles One or more Docker Compose configuration files */ public DockerComposeContainer(DockerImageName image, String identifier, File... composeFiles) { this(image, identifier, Arrays.asList(composeFiles)); } /** * Creates a new DockerComposeContainer with the specified Docker image, identifier, and a single compose file. * * @param image The Docker image to use for the container * @param identifier A unique identifier for this compose environment * @param composeFile A Docker Compose configuration file */ public DockerComposeContainer(DockerImageName image, String identifier, File composeFile) { this(image, identifier, Collections.singletonList(composeFile)); } /** * Creates a new DockerComposeContainer with the specified Docker image, identifier, and compose files. * * @param image The Docker image to use for the container * @param identifier A unique identifier for this compose environment * @param composeFiles A list of Docker Compose configuration files */ public DockerComposeContainer(DockerImageName image, String identifier, List composeFiles) { this.composeDelegate = new ComposeDelegate(ComposeDelegate.ComposeVersion.V1, composeFiles, identifier, COMPOSE_EXECUTABLE, image); this.project = this.composeDelegate.getProject(); } /** * Use the new constructor {@link #DockerComposeContainer(DockerImageName image, String identifier, File composeFile)} */ public DockerComposeContainer(File composeFile, String identifier) { this(identifier, composeFile); this.localCompose = true; } /** * Use the new constructor {@link #DockerComposeContainer(DockerImageName image, List composeFiles)} */ public DockerComposeContainer(File... composeFiles) { this(Arrays.asList(composeFiles)); this.localCompose = true; } /** * Use the new constructor {@link #DockerComposeContainer(DockerImageName image, List composeFiles)} */ @Deprecated public DockerComposeContainer(List composeFiles) { this(Base58.randomString(6).toLowerCase(), composeFiles); this.localCompose = true; } /** * Use the new constructor {@link #DockerComposeContainer(DockerImageName image, String identifier, File... composeFiles)} */ public DockerComposeContainer(String identifier, File... composeFiles) { this(identifier, Arrays.asList(composeFiles)); this.localCompose = true; } /** * Use the new constructor {@link #DockerComposeContainer(DockerImageName image, String identifier, List composeFiles)} */ public DockerComposeContainer(String identifier, List composeFiles) { this.composeDelegate = new ComposeDelegate( ComposeDelegate.ComposeVersion.V1, composeFiles, identifier, COMPOSE_EXECUTABLE, DockerImageName.parse("docker/compose:1.29.2") ); this.project = this.composeDelegate.getProject(); this.localCompose = true; } @Override public void start() { synchronized (MUTEX) { this.composeDelegate.registerContainersForShutdown(); if (pull) { try { this.composeDelegate.pullImages(); } catch (ContainerLaunchException e) { log.warn("Exception while pulling images, using local images if available", e); } } this.composeDelegate.createServices( this.localCompose, this.build, this.options, this.services, this.scalingPreferences, this.env, this.filesInDirectory ); this.composeDelegate.startAmbassadorContainer(); this.composeDelegate.waitUntilServiceStarted(this.tailChildContainers); } } @VisibleForTesting List listChildContainers() { return this.composeDelegate.listChildContainers(); } public SELF withServices(@NonNull String... services) { this.services = Arrays.asList(services); return self(); } @Override public void stop() { synchronized (MUTEX) { try { this.composeDelegate.getAmbassadorContainer().stop(); // Kill the services using docker-compose String cmd = ComposeCommand.getDownCommand(ComposeDelegate.ComposeVersion.V1, this.options); if (removeVolumes) { cmd += " -v"; } if (removeImages != null) { cmd += " --rmi " + removeImages.dockerRemoveImagesType(); } this.composeDelegate.runWithCompose(this.localCompose, cmd, this.env, this.filesInDirectory); } finally { this.composeDelegate.clear(); this.project = this.composeDelegate.randomProjectId(); } } } public SELF withExposedService(String serviceName, int servicePort) { this.composeDelegate.withExposedService(serviceName, servicePort, Wait.defaultWaitStrategy()); return self(); } public DockerComposeContainer withExposedService(String serviceName, int instance, int servicePort) { return withExposedService(serviceName + "_" + instance, servicePort); } public DockerComposeContainer withExposedService( String serviceName, int instance, int servicePort, WaitStrategy waitStrategy ) { this.composeDelegate.withExposedService(serviceName + "_" + instance, servicePort, waitStrategy); return self(); } public SELF withExposedService(String serviceName, int servicePort, @NonNull WaitStrategy waitStrategy) { this.composeDelegate.withExposedService(serviceName, servicePort, waitStrategy); return self(); } /** * Specify the {@link WaitStrategy} to use to determine if the container is ready. * * @param serviceName the name of the service to wait for * @param waitStrategy the WaitStrategy to use * @return this * @see org.testcontainers.containers.wait.strategy.Wait#defaultWaitStrategy() */ public SELF waitingFor(String serviceName, @NonNull WaitStrategy waitStrategy) { String serviceInstanceName = this.composeDelegate.getServiceInstanceName(serviceName); this.composeDelegate.addWaitStrategy(serviceInstanceName, waitStrategy); return self(); } /** * Get the host (e.g. IP address or hostname) that an exposed service can be found at, from the host machine * (i.e. should be the machine that's running this Java process). *

* The service must have been declared using DockerComposeContainer#withExposedService. * * @param serviceName the name of the service as set in the docker-compose.yml file. * @param servicePort the port exposed by the service container. * @return a host IP address or hostname that can be used for accessing the service container. */ public String getServiceHost(String serviceName, Integer servicePort) { return this.composeDelegate.getServiceHost(); } /** * Get the port that an exposed service can be found at, from the host machine * (i.e. should be the machine that's running this Java process). *

* The service must have been declared using DockerComposeContainer#withExposedService. * * @param serviceName the name of the service as set in the docker-compose.yml file. * @param servicePort the port exposed by the service container. * @return a port that can be used for accessing the service container. */ public Integer getServicePort(String serviceName, Integer servicePort) { return this.composeDelegate.getServicePort(serviceName, servicePort); } public SELF withScaledService(String serviceBaseName, int numInstances) { scalingPreferences.put(serviceBaseName, numInstances); return self(); } public SELF withEnv(String key, String value) { env.put(key, value); return self(); } public SELF withEnv(Map env) { env.forEach(this.env::put); return self(); } /** * Whether to pull images first. * * @return this instance, for chaining */ public SELF withPull(boolean pull) { this.pull = pull; return self(); } /** * Whether to tail child container logs. * * @return this instance, for chaining */ public SELF withTailChildContainers(boolean tailChildContainers) { this.tailChildContainers = tailChildContainers; return self(); } /** * Attach an output consumer at container startup, enabling stdout and stderr to be followed, waited on, etc. *

* More than one consumer may be registered. * * @param serviceName the name of the service as set in the docker-compose.yml file * @param consumer consumer that output frames should be sent to * @return this instance, for chaining */ public SELF withLogConsumer(String serviceName, Consumer consumer) { this.composeDelegate.withLogConsumer(serviceName, consumer); return self(); } /** * Whether to always build images before starting containers. * * @return this instance, for chaining */ public SELF withBuild(boolean build) { this.build = build; return self(); } /** * Adds options to the docker-compose command, e.g. docker-compose --compatibility. * * @return this instance, for chaining */ public SELF withOptions(String... options) { this.options = new HashSet<>(Arrays.asList(options)); return self(); } /** * Remove images after containers shutdown. * * @return this instance, for chaining */ public SELF withRemoveImages(RemoveImages removeImages) { this.removeImages = removeImages; return self(); } /** * Remove volumes after containers shut down. * * @param removeVolumes whether volumes are to be removed. * @return this instance, for chaining. */ public SELF withRemoveVolumes(boolean removeVolumes) { this.removeVolumes = removeVolumes; return self(); } /** * Set the maximum startup timeout all the waits set are bounded to. * * @return this instance. for chaining */ public SELF withStartupTimeout(Duration startupTimeout) { this.composeDelegate.setStartupTimeout(startupTimeout); return self(); } public SELF withCopyFilesInContainer(String... fileCopyInclusions) { this.filesInDirectory = Arrays.asList(fileCopyInclusions); return self(); } public Optional getContainerByServiceName(String serviceName) { return this.composeDelegate.getContainerByServiceName(serviceName); } private void followLogs(String containerId, Consumer consumer) { this.followLogs(containerId, consumer); } private SELF self() { return (SELF) this; } public enum RemoveImages { /** * Remove all images used by any service. */ ALL("all"), /** * Remove only images that don't have a custom tag set by the `image` field. */ LOCAL("local"); private final String dockerRemoveImagesType; RemoveImages(final String dockerRemoveImagesType) { this.dockerRemoveImagesType = dockerRemoveImagesType; } public String dockerRemoveImagesType() { return dockerRemoveImagesType; } } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/DockerComposeFiles.java ================================================ package org.testcontainers.containers; import org.testcontainers.utility.DockerImageName; import java.io.File; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; public class DockerComposeFiles { private final List parsedComposeFiles; public DockerComposeFiles(List composeFiles) { this.parsedComposeFiles = composeFiles.stream().map(ParsedDockerComposeFile::new).collect(Collectors.toList()); } public Set getDependencyImages() { Map> mergedServiceNameToImageNames = mergeServiceDependencyImageNames(); return getImageNames(mergedServiceNameToImageNames); } private Map> mergeServiceDependencyImageNames() { Map> mergedServiceNameToImageNames = new HashMap<>(); for (ParsedDockerComposeFile parsedComposeFile : parsedComposeFiles) { mergedServiceNameToImageNames.putAll(parsedComposeFile.getServiceNameToImageNames()); } return mergedServiceNameToImageNames; } private Set getImageNames(Map> serviceToImageNames) { return serviceToImageNames .values() .stream() .flatMap(Collection::stream) // Pass through DockerImageName to convert image names to canonical form (e.g. making implicit latest tag explicit) .map(DockerImageName::parse) .map(DockerImageName::asCanonicalNameString) .collect(Collectors.toSet()); } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/DockerMcpGatewayContainer.java ================================================ package org.testcontainers.containers; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.DockerImageName; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Testcontainers implementation of the Docker MCP Gateway container. *

* Supported images: {@code docker/mcp-gateway} *

* Exposed ports: 8811 */ public class DockerMcpGatewayContainer extends GenericContainer { private static final String DOCKER_MCP_GATEWAY_IMAGE = "docker/mcp-gateway"; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(DOCKER_MCP_GATEWAY_IMAGE); private static final int DEFAULT_PORT = 8811; private static final String SECRETS_PATH = "/testcontainers/app/secrets"; private final List servers = new ArrayList<>(); private final List tools = new ArrayList<>(); private final Map secrets = new HashMap<>(); public DockerMcpGatewayContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public DockerMcpGatewayContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); withExposedPorts(DEFAULT_PORT); withFileSystemBind(DockerClientFactory.instance().getRemoteDockerUnixSocketPath(), "/var/run/docker.sock"); waitingFor(Wait.forLogMessage(".*Start sse server on port.*", 1)); } @Override protected void configure() { List command = new ArrayList<>(); command.add("--transport=sse"); for (String server : this.servers) { if (!server.isEmpty()) { command.add("--servers=" + server); } } for (String tool : this.tools) { if (!tool.isEmpty()) { command.add("--tools=" + tool); } } if (this.secrets != null && !this.secrets.isEmpty()) { command.add("--secrets=" + SECRETS_PATH); } withCommand(String.join(" ", command)); } @Override protected void containerIsCreated(String containerId) { if (this.secrets != null && !this.secrets.isEmpty()) { StringBuilder secretsFile = new StringBuilder(); for (Map.Entry entry : this.secrets.entrySet()) { secretsFile.append(entry.getKey()).append("=").append(entry.getValue()).append("\n"); } copyFileToContainer(Transferable.of(secretsFile.toString()), SECRETS_PATH); } } public DockerMcpGatewayContainer withServer(String server, List tools) { this.servers.add(server); this.tools.addAll(tools); return this; } public DockerMcpGatewayContainer withServer(String server, String... tools) { this.servers.add(server); this.tools.addAll(Arrays.asList(tools)); return this; } public DockerMcpGatewayContainer withSecrets(Map secrets) { this.secrets.putAll(secrets); return this; } public DockerMcpGatewayContainer withSecret(String secretKey, String secretValue) { this.secrets.put(secretKey, secretValue); return this; } public String getEndpoint() { return "http://" + getHost() + ":" + getMappedPort(DEFAULT_PORT); } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/DockerModelRunnerContainer.java ================================================ package org.testcontainers.containers; import com.github.dockerjava.api.command.InspectContainerResponse; import lombok.extern.slf4j.Slf4j; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; /** * Testcontainers proxy container for the Docker Model Runner service * provided by Docker Desktop. *

* Supported images: {@code alpine/socat} *

* Exposed ports: 80 */ @Slf4j public class DockerModelRunnerContainer extends SocatContainer { private static final String MODEL_RUNNER_ENDPOINT = "model-runner.docker.internal"; private static final int PORT = 80; private String model; public DockerModelRunnerContainer(String image) { this(DockerImageName.parse(image)); } public DockerModelRunnerContainer(DockerImageName image) { super(image); withTarget(PORT, MODEL_RUNNER_ENDPOINT); waitingFor(Wait.forHttp("/").forResponsePredicate(res -> res.contains("The service is running"))); } @Override protected void containerIsStarted(InspectContainerResponse containerInfo) { if (this.model != null) { logger().info("Pulling model: {}. Please be patient.", this.model); String url = getBaseEndpoint() + "/models/create"; String payload = String.format("{\"from\": \"%s\"}", this.model); try { HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); connection.setRequestMethod("POST"); connection.setRequestProperty("Content-Type", "application/json"); connection.setDoOutput(true); try (OutputStream os = connection.getOutputStream()) { os.write(payload.getBytes()); os.flush(); } try ( BufferedReader br = new BufferedReader( new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8) ) ) { while (br.readLine() != null) {} } connection.disconnect(); } catch (IOException e) { logger().error("Failed to pull model {}: {}", this.model, e); } logger().info("Finished pulling model: {}", this.model); } } public DockerModelRunnerContainer withModel(String model) { this.model = model; return this; } /** * Returns the base endpoint URL for the Docker Model Runner service. * * @return the base URL in the format {@code http://:} */ public String getBaseEndpoint() { return "http://" + getHost() + ":" + getMappedPort(PORT); } /** * Returns the OpenAI-compatible API endpoint URL for the Docker Model Runner service. * * @return the OpenAI-compatible endpoint URL in the format {@code http://:/engines} */ public String getOpenAIEndpoint() { return getBaseEndpoint() + "/engines"; } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/ExecConfig.java ================================================ package org.testcontainers.containers; import lombok.Builder; import lombok.Getter; import java.util.Map; /** * Exec configuration. */ @Builder @Getter public class ExecConfig { /** * The command to run. */ private String[] command; /** * The user to run the exec process. */ private String user; /** * Key-value pairs of environment variables. */ private Map envVars; /** * The working directory for the exec process. */ private String workDir; } ================================================ FILE: core/src/main/java/org/testcontainers/containers/ExecInContainerPattern.java ================================================ package org.testcontainers.containers; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.ExecCreateCmd; import com.github.dockerjava.api.command.ExecCreateCmdResponse; import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.api.exception.DockerException; import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.output.FrameConsumerResultCallback; import org.testcontainers.containers.output.OutputFrame; import org.testcontainers.containers.output.ToStringConsumer; import org.testcontainers.utility.TestEnvironment; import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** * Provides utility methods for executing commands in containers */ @UtilityClass @Slf4j public class ExecInContainerPattern { /** * * @deprecated use {@link #execInContainer(DockerClient, InspectContainerResponse, String...)} */ @Deprecated public Container.ExecResult execInContainer(InspectContainerResponse containerInfo, String... command) throws UnsupportedOperationException, IOException, InterruptedException { DockerClient dockerClient = DockerClientFactory.instance().client(); return execInContainer(dockerClient, containerInfo, command); } /** * * @deprecated use {@link #execInContainer(DockerClient, InspectContainerResponse, Charset, String...)} */ @Deprecated public Container.ExecResult execInContainer( InspectContainerResponse containerInfo, Charset outputCharset, String... command ) throws UnsupportedOperationException, IOException, InterruptedException { DockerClient dockerClient = DockerClientFactory.instance().client(); return execInContainerWithUser(dockerClient, containerInfo, outputCharset, null, command); } /** * Run a command inside a running container, as though using "docker exec", and interpreting * the output as UTF8. *

* @param dockerClient the {@link DockerClient} * @param containerInfo the container info * @param command the command to execute * @see #execInContainerWithUser(DockerClient, InspectContainerResponse, String, String...) */ public Container.ExecResult execInContainer( DockerClient dockerClient, InspectContainerResponse containerInfo, String... command ) throws UnsupportedOperationException, IOException, InterruptedException { return execInContainerWithUser(dockerClient, containerInfo, StandardCharsets.UTF_8, null, command); } /** * Run a command inside a running container, as though using "docker exec", and interpreting * the output as UTF8. *

* @param dockerClient the {@link DockerClient} * @param containerInfo the container info * @param outputCharset the character set used to interpret the output. * @param command the command to execute * @see #execInContainerWithUser(DockerClient, InspectContainerResponse, Charset, String, String...) */ public Container.ExecResult execInContainer( DockerClient dockerClient, InspectContainerResponse containerInfo, Charset outputCharset, String... command ) throws UnsupportedOperationException, IOException, InterruptedException { return execInContainerWithUser(dockerClient, containerInfo, outputCharset, null, command); } /** * Run a command inside a running container as a given user, as using "docker exec -u user" and * interpreting the output as UTF8. *

* This functionality is not available on a docker daemon running the older "lxc" execution driver. At * the time of writing, CircleCI was using this driver. * @param dockerClient the {@link DockerClient} * @param containerInfo the container info * @param user the user to run the command with, optional * @param command the command to execute * @see #execInContainerWithUser(DockerClient, InspectContainerResponse, Charset, String, * String...) * @deprecated use {@link #execInContainer(DockerClient, InspectContainerResponse, ExecConfig)} */ @Deprecated public Container.ExecResult execInContainerWithUser( DockerClient dockerClient, InspectContainerResponse containerInfo, String user, String... command ) throws UnsupportedOperationException, IOException, InterruptedException { return execInContainerWithUser(dockerClient, containerInfo, StandardCharsets.UTF_8, user, command); } /** * Run a command inside a running container as a given user, as using "docker exec -u user". *

* This functionality is not available on a docker daemon running the older "lxc" execution * driver. At the time of writing, CircleCI was using this driver. * @param dockerClient the {@link DockerClient} * @param containerInfo the container info * @param outputCharset the character set used to interpret the output. * @param user the user to run the command with, optional * @param command the parts of the command to run * @return the result of execution * @throws IOException if there's an issue communicating with Docker * @throws InterruptedException if the thread waiting for the response is interrupted * @throws UnsupportedOperationException if the docker daemon you're connecting to doesn't support "exec". * @deprecated use {@link #execInContainer(DockerClient, InspectContainerResponse, Charset, ExecConfig)} */ @Deprecated public Container.ExecResult execInContainerWithUser( DockerClient dockerClient, InspectContainerResponse containerInfo, Charset outputCharset, String user, String... command ) throws UnsupportedOperationException, IOException, InterruptedException { return execInContainer( dockerClient, containerInfo, outputCharset, ExecConfig.builder().user(user).command(command).build() ); } /** * Run a command inside a running container as a given user, as using "docker exec -u user". *

* This functionality is not available on a docker daemon running the older "lxc" execution * driver. At the time of writing, CircleCI was using this driver. * @param dockerClient the {@link DockerClient} * @param containerInfo the container info * @param execConfig the exec configuration * @return the result of execution * @throws IOException if there's an issue communicating with Docker * @throws InterruptedException if the thread waiting for the response is interrupted * @throws UnsupportedOperationException if the docker daemon you're connecting to doesn't support "exec". */ public Container.ExecResult execInContainer( DockerClient dockerClient, InspectContainerResponse containerInfo, ExecConfig execConfig ) throws UnsupportedOperationException, IOException, InterruptedException { return execInContainer(dockerClient, containerInfo, StandardCharsets.UTF_8, execConfig); } /** * Run a command inside a running container as a given user, as using "docker exec -u user". *

* This functionality is not available on a docker daemon running the older "lxc" execution * driver. At the time of writing, CircleCI was using this driver. * @param dockerClient the {@link DockerClient} * @param containerInfo the container info * @param outputCharset the character set used to interpret the output. * @param execConfig the exec configuration * @return the result of execution * @throws IOException if there's an issue communicating with Docker * @throws InterruptedException if the thread waiting for the response is interrupted * @throws UnsupportedOperationException if the docker daemon you're connecting to doesn't support "exec". */ public Container.ExecResult execInContainer( DockerClient dockerClient, InspectContainerResponse containerInfo, Charset outputCharset, ExecConfig execConfig ) throws UnsupportedOperationException, IOException, InterruptedException { if (!TestEnvironment.dockerExecutionDriverSupportsExec()) { // at time of writing, this is the expected result in CircleCI. throw new UnsupportedOperationException( "Your docker daemon is running the \"lxc\" driver, which doesn't support \"docker exec\"." ); } if (!isRunning(containerInfo)) { throw new IllegalStateException("execInContainer can only be used while the Container is running"); } String containerId = containerInfo.getId(); String containerName = containerInfo.getName(); String[] command = execConfig.getCommand(); log.debug("{}: Running \"exec\" command: {}", containerName, String.join(" ", command)); final ExecCreateCmd execCreateCmd = dockerClient .execCreateCmd(containerId) .withAttachStdout(true) .withAttachStderr(true) .withCmd(command); String user = execConfig.getUser(); if (user != null && !user.isEmpty()) { log.debug("{}: Running \"exec\" command with user: {}", containerName, user); execCreateCmd.withUser(user); } String workDir = execConfig.getWorkDir(); if (workDir != null && !workDir.isEmpty()) { log.debug("{}: Running \"exec\" command inside workingDir: {}", containerName, workDir); execCreateCmd.withWorkingDir(workDir); } Map envVars = execConfig.getEnvVars(); if (envVars != null && !envVars.isEmpty()) { List envVarList = envVars .entrySet() .stream() .map(e -> e.getKey() + "=" + e.getValue()) .collect(Collectors.toList()); execCreateCmd.withEnv(envVarList); } final ExecCreateCmdResponse execCreateCmdResponse = execCreateCmd.exec(); final ToStringConsumer stdoutConsumer = new ToStringConsumer(); final ToStringConsumer stderrConsumer = new ToStringConsumer(); try (FrameConsumerResultCallback callback = new FrameConsumerResultCallback()) { callback.addConsumer(OutputFrame.OutputType.STDOUT, stdoutConsumer); callback.addConsumer(OutputFrame.OutputType.STDERR, stderrConsumer); dockerClient.execStartCmd(execCreateCmdResponse.getId()).exec(callback).awaitCompletion(); } int exitCode = dockerClient.inspectExecCmd(execCreateCmdResponse.getId()).exec().getExitCodeLong().intValue(); final Container.ExecResult result = new Container.ExecResult( exitCode, stdoutConsumer.toString(outputCharset), stderrConsumer.toString(outputCharset) ); log.trace("{}: stdout: {}", containerName, result.getStdout()); log.trace("{}: stderr: {}", containerName, result.getStderr()); return result; } private boolean isRunning(InspectContainerResponse containerInfo) { try { return containerInfo != null && containerInfo.getState().getRunning(); } catch (DockerException e) { return false; } } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/FixedHostPortGenericContainer.java ================================================ package org.testcontainers.containers; import org.jetbrains.annotations.NotNull; /** * Variant of {@link GenericContainer} that allows a fixed port on the docker host to be mapped to a container port. * *

Normally this should not be required, and Docker should be allowed to choose a free host port instead. * However, when a fixed host port is absolutely required for some reason, this class can be used to set it.

* *

Callers are responsible for ensuring that this fixed port is actually available; failure will occur if it is * not available - which could manifest as flaky or unstable tests.

*/ public class FixedHostPortGenericContainer> extends GenericContainer { /** * @deprecated it is highly recommended that {@link FixedHostPortGenericContainer} not be used, as it risks port conflicts. */ @Deprecated public FixedHostPortGenericContainer(@NotNull String dockerImageName) { super(dockerImageName); } /** * Bind a fixed TCP port on the docker host to a container port * @param hostPort a port on the docker host, which must be available * @param containerPort a port in the container * @return this container */ public SELF withFixedExposedPort(int hostPort, int containerPort) { return withFixedExposedPort(hostPort, containerPort, InternetProtocol.TCP); } /** * Bind a fixed port on the docker host to a container port * @param hostPort a port on the docker host, which must be available * @param containerPort a port in the container * @param protocol an internet protocol (tcp or udp) * @return this container */ public SELF withFixedExposedPort(int hostPort, int containerPort, InternetProtocol protocol) { super.addFixedExposedPort(hostPort, containerPort, protocol); return self(); } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/FutureContainer.java ================================================ package org.testcontainers.containers; import lombok.Data; import org.testcontainers.containers.traits.LinkableContainer; /** * A container that may not have been launched yet. */ @Data public class FutureContainer implements LinkableContainer { private final String containerName; } ================================================ FILE: core/src/main/java/org/testcontainers/containers/GenericContainer.java ================================================ package org.testcontainers.containers; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.SerializationFeature; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.CreateContainerCmd; import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.api.exception.NotFoundException; import com.github.dockerjava.api.model.Bind; import com.github.dockerjava.api.model.ContainerNetwork; import com.github.dockerjava.api.model.ExposedPort; import com.github.dockerjava.api.model.HostConfig; import com.github.dockerjava.api.model.Link; import com.github.dockerjava.api.model.PortBinding; import com.github.dockerjava.api.model.Ports; import com.github.dockerjava.api.model.Volume; import com.github.dockerjava.api.model.VolumesFrom; import com.github.dockerjava.core.DefaultDockerClientConfig; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.hash.Hashing; import lombok.AccessLevel; import lombok.Data; import lombok.Getter; import lombok.NonNull; import lombok.Setter; import lombok.SneakyThrows; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.rnorth.ducttape.unreliables.Unreliables; import org.slf4j.Logger; import org.testcontainers.DockerClientFactory; import org.testcontainers.UnstableAPI; import org.testcontainers.containers.output.OutputFrame; import org.testcontainers.containers.startupcheck.IsRunningStartupCheckStrategy; import org.testcontainers.containers.startupcheck.MinimumDurationRunningStartupCheckStrategy; import org.testcontainers.containers.startupcheck.StartupCheckStrategy; import org.testcontainers.containers.traits.LinkableContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.containers.wait.strategy.WaitStrategy; import org.testcontainers.containers.wait.strategy.WaitStrategyTarget; import org.testcontainers.core.CreateContainerCmdModifier; import org.testcontainers.images.ImagePullPolicy; import org.testcontainers.images.RemoteDockerImage; import org.testcontainers.images.builder.Transferable; import org.testcontainers.lifecycle.Startable; import org.testcontainers.lifecycle.Startables; import org.testcontainers.utility.Base58; import org.testcontainers.utility.CommandLine; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.DockerLoggerFactory; import org.testcontainers.utility.DockerMachineClient; import org.testcontainers.utility.DynamicPollInterval; import org.testcontainers.utility.MountableFile; import org.testcontainers.utility.PathUtils; import org.testcontainers.utility.ResourceReaper; import org.testcontainers.utility.TestcontainersConfiguration; import java.io.File; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.UndeclaredThrowableException; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; import java.util.ServiceLoader; import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.zip.Adler32; import java.util.zip.Checksum; import static org.awaitility.Awaitility.await; /** * Base class for that allows a container to be launched and controlled. */ @Data public class GenericContainer> implements Container, AutoCloseable, WaitStrategyTarget, Startable { public static final int CONTAINER_RUNNING_TIMEOUT_SEC = 30; public static final String INTERNAL_HOST_HOSTNAME = "host.testcontainers.internal"; static final String HASH_LABEL = "org.testcontainers.hash"; static final String COPIED_FILES_HASH_LABEL = "org.testcontainers.copied_files.hash"; /* * Default settings */ @NonNull private List extraHosts = new ArrayList<>(); @NonNull private RemoteDockerImage image; @NonNull private List volumesFroms = new ArrayList<>(); /** * @deprecated Links are deprecated (see #465). Please use {@link Network} features instead. */ @NonNull @Deprecated private Map linkedContainers = new HashMap<>(); private StartupCheckStrategy startupCheckStrategy = new IsRunningStartupCheckStrategy(); private int startupAttempts = 1; @Nullable private String workingDirectory = null; /** * The shared memory size to use when starting the container. * This value is in bytes. */ @Nullable private Long shmSize; // Maintain order in which entries are added, as earlier target location may be a prefix of a later location. @Deprecated private Map copyToFileContainerPathMap = new LinkedHashMap<>(); // Maintain order in which entries are added, as earlier target location may be a prefix of a later location. @Setter(AccessLevel.NONE) @Getter(AccessLevel.MODULE) @VisibleForTesting private Map copyToTransferableContainerPathMap = new LinkedHashMap<>(); protected final Set dependencies = new HashSet<>(); /** * Unique instance of DockerClient for use by this container object. * We use {@link DockerClientFactory#lazyClient()} here to avoid eager client creation */ @Setter(AccessLevel.NONE) protected DockerClient dockerClient = DockerClientFactory.lazyClient(); /** * Set during container startup */ @Setter(AccessLevel.NONE) @VisibleForTesting String containerId; @Setter(AccessLevel.NONE) private InspectContainerResponse containerInfo; static WaitStrategy DEFAULT_WAIT_STRATEGY = Wait.defaultWaitStrategy(); /** * The approach to determine if the container is ready. */ @NonNull protected WaitStrategy waitStrategy = DEFAULT_WAIT_STRATEGY; private List> logConsumers = new ArrayList<>(); private static final Set AVAILABLE_IMAGE_NAME_CACHE = new HashSet<>(); @Nullable private Map tmpFsMapping; @Setter(AccessLevel.NONE) private boolean shouldBeReused = false; private boolean hostAccessible = false; private final Set createContainerCmdModifiers = loadCreateContainerCmdCustomizers(); private ContainerDef containerDef; ContainerDef createContainerDef() { return new ContainerDef(); } ContainerDef getContainerDef() { return this.containerDef; } private Set loadCreateContainerCmdCustomizers() { ServiceLoader containerCmdCustomizers = ServiceLoader.load( CreateContainerCmdModifier.class ); Set loadedCustomizers = new LinkedHashSet<>(); for (CreateContainerCmdModifier customizer : containerCmdCustomizers) { loadedCustomizers.add(customizer); } return loadedCustomizers; } public GenericContainer(@NonNull final DockerImageName dockerImageName) { this(new RemoteDockerImage(dockerImageName)); } public GenericContainer(@NonNull final RemoteDockerImage image) { this.image = image; this.containerDef = createContainerDef(); this.containerDef.addNetworkAlias("tc-" + Base58.randomString(8)); this.containerDef.setImage(image); } /** * @deprecated use {@link #GenericContainer(DockerImageName)} instead */ @Deprecated public GenericContainer() { this(TestcontainersConfiguration.getInstance().getTinyImage()); } public GenericContainer(@NonNull final String dockerImageName) { this(new RemoteDockerImage(DockerImageName.parse(dockerImageName))); } public GenericContainer(@NonNull final Future image) { this(new RemoteDockerImage(image)); } GenericContainer(@NonNull final ContainerDef containerDef) { this.image = containerDef.getImage(); this.containerDef = containerDef; } public void setImage(Future image) { this.image = new RemoteDockerImage(image); this.containerDef.setImage(new RemoteDockerImage(image)); } @Override public List getExposedPorts() { List exposedPorts = new ArrayList<>(); for (ExposedPort exposedPort : this.containerDef.getExposedPorts()) { exposedPorts.add(exposedPort.getPort()); } return exposedPorts; } @Override public void setExposedPorts(List exposedPorts) { this.containerDef.exposedPorts.clear(); for (Integer exposedPort : exposedPorts) { this.containerDef.addExposedTcpPort(exposedPort); } } /** * @see #dependsOn(Iterable) */ public SELF dependsOn(Startable... startables) { Collections.addAll(dependencies, startables); return self(); } /** * @see #dependsOn(Iterable) */ public SELF dependsOn(List startables) { return this.dependsOn((Iterable) startables); } /** * Delays this container's creation and start until provided {@link Startable}s start first. * Note that the circular dependencies are not supported. * * @param startables a list of {@link Startable} to depend on * @see Startables#deepStart(Iterable) */ public SELF dependsOn(Iterable startables) { startables.forEach(dependencies::add); return self(); } public String getContainerId() { return containerId; } /** * Starts the container using docker, pulling an image if necessary. */ @Override @SneakyThrows({ InterruptedException.class, ExecutionException.class }) public void start() { if (containerId != null) { return; } Startables.deepStart(dependencies).get(); // trigger LazyDockerClient's resolve so that we fail fast here and not in getDockerImageName() dockerClient.authConfig(); doStart(); } protected void doStart() { try { if (this.waitStrategy != DEFAULT_WAIT_STRATEGY) { this.containerDef.setWaitStrategy(this.waitStrategy); } configure(); logger().debug("Starting container: {}", getDockerImageName()); AtomicInteger attempt = new AtomicInteger(0); Unreliables.retryUntilSuccess( startupAttempts, () -> { logger() .debug( "Trying to start container: {} (attempt {}/{})", getDockerImageName(), attempt.incrementAndGet(), startupAttempts ); tryStart(); return true; } ); } catch (Exception e) { throw new ContainerLaunchException("Container startup failed for image " + getDockerImageName(), e); } } @UnstableAPI @SneakyThrows protected boolean canBeReused() { for (Class type = getClass(); type != GenericContainer.class; type = type.getSuperclass()) { try { Method method = type.getDeclaredMethod("containerIsCreated", String.class); if (method.getDeclaringClass() != GenericContainer.class) { logger().warn("{} can't be reused because it overrides {}", getClass(), method.getName()); return false; } } catch (NoSuchMethodException | NoClassDefFoundError e) { // ignore } } return true; } private void tryStart() { try { String dockerImageName = getDockerImageName(); logger().debug("Starting container: {}", dockerImageName); Instant startedAt = Instant.now(); logger().info("Creating container for image: {}", dockerImageName); CreateContainerCmd createCommand = dockerClient.createContainerCmd(dockerImageName); applyConfiguration(createCommand); createCommand.getLabels().putAll(DockerClientFactory.DEFAULT_LABELS); boolean reused = false; final boolean reusable; if (shouldBeReused) { if (!canBeReused()) { throw new IllegalStateException("This container does not support reuse"); } if (TestcontainersConfiguration.getInstance().environmentSupportsReuse()) { createCommand .getLabels() .put(COPIED_FILES_HASH_LABEL, Long.toHexString(hashCopiedFiles().getValue())); String hash = hash(createCommand); containerId = findContainerForReuse(hash).orElse(null); if (containerId != null) { logger().info("Reusing container with ID: {} and hash: {}", containerId, hash); reused = true; } else { logger().debug("Can't find a reusable running container with hash: {}", hash); createCommand.getLabels().put(HASH_LABEL, hash); } reusable = true; } else { logger() .warn( "" + "Reuse was requested but the environment does not support the reuse of containers\n" + "To enable reuse of containers, you must set 'testcontainers.reuse.enable=true' in a file located at {}", Paths.get(System.getProperty("user.home"), ".testcontainers.properties") ); reusable = false; } } else { reusable = false; } if (!reusable) { //noinspection deprecation createCommand = ResourceReaper.instance().register(this, createCommand); } if (!reused) { containerId = createCommand.exec().getId(); // TODO use single "copy" invocation (and calculate an hash of the resulting tar archive) copyToFileContainerPathMap.forEach(this::copyFileToContainer); copyToTransferableContainerPathMap.forEach(this::copyFileToContainer); } connectToPortForwardingNetwork(createCommand.getNetworkMode()); if (!reused) { containerIsCreated(containerId); logger().info("Container {} is starting: {}", dockerImageName, containerId); dockerClient.startContainerCmd(containerId).exec(); } else { logger().info("Reusing existing container ({}) and not creating a new one", containerId); } // For all registered output consumers, start following as close to container startup as possible this.logConsumers.forEach(this::followOutput); // Wait until inspect container returns the mapped ports containerInfo = await() .atMost(5, TimeUnit.SECONDS) .pollInterval(DynamicPollInterval.ofMillis(50)) .pollInSameThread() .until( () -> dockerClient.inspectContainerCmd(containerId).exec(), inspectContainerResponse -> { Set exposedAndMappedPorts = inspectContainerResponse .getNetworkSettings() .getPorts() .getBindings() .entrySet() .stream() .filter(it -> Objects.nonNull(it.getValue())) // filter out exposed but not yet mapped .map(Entry::getKey) .collect(Collectors.toSet()); return exposedAndMappedPorts.containsAll(this.containerDef.getExposedPorts()); } ); String emulationWarning = checkForEmulation(); if (emulationWarning != null) { logger().warn(emulationWarning); } // Tell subclasses that we're starting containerIsStarting(containerInfo, reused); // Wait until the container has reached the desired running state if (!this.startupCheckStrategy.waitUntilStartupSuccessful(this)) { // Bail out, don't wait for the port to start listening. // (Exception thrown here will be caught below and wrapped) throw new IllegalStateException("Container did not start correctly."); } // Wait until the process within the container has become ready for use (e.g. listening on network, log message emitted, etc). try { waitUntilContainerStarted(); } catch (Exception e) { logger().debug("Wait strategy threw an exception", e); InspectContainerResponse inspectContainerResponse = null; try { inspectContainerResponse = dockerClient.inspectContainerCmd(containerId).exec(); } catch (NotFoundException notFoundException) { logger().debug("Container {} not found", containerId, notFoundException); } if (inspectContainerResponse == null) { throw new IllegalStateException("Wait strategy failed. Container is removed", e); } InspectContainerResponse.ContainerState state = inspectContainerResponse.getState(); if (Boolean.TRUE.equals(state.getDead())) { throw new IllegalStateException("Wait strategy failed. Container is dead", e); } if (Boolean.TRUE.equals(state.getOOMKilled())) { throw new IllegalStateException( "Wait strategy failed. Container crashed with out-of-memory (OOMKilled)", e ); } String error = state.getError(); if (!StringUtils.isBlank(error)) { throw new IllegalStateException("Wait strategy failed. Container crashed: " + error, e); } if (!Boolean.TRUE.equals(state.getRunning())) { throw new IllegalStateException( "Wait strategy failed. Container exited with code " + state.getExitCode(), e ); } throw e; } logger().info("Container {} started in {}", dockerImageName, Duration.between(startedAt, Instant.now())); containerIsStarted(containerInfo, reused); } catch (Exception e) { if (e instanceof UndeclaredThrowableException && e.getCause() instanceof Exception) { e = (Exception) e.getCause(); } if (e instanceof InvocationTargetException && e.getCause() instanceof Exception) { e = (Exception) e.getCause(); } logger().error("Could not start container", e); if (containerId != null) { // Log output if startup failed, either due to a container failure or exception (including timeout) final String containerLogs = getLogs(); if (containerLogs.length() > 0) { logger().error("Log output from the failed container:\n{}", getLogs()); } else { logger().error("There are no stdout/stderr logs available for the failed container"); } stop(); } throw new ContainerLaunchException("Could not create/start container", e); } } @VisibleForTesting Checksum hashCopiedFiles() { Checksum checksum = new Adler32(); Stream .of(copyToFileContainerPathMap, copyToTransferableContainerPathMap) .flatMap(it -> it.entrySet().stream()) .sorted(Entry.comparingByValue()) .forEach(entry -> { byte[] pathBytes = entry.getValue().getBytes(); // Add path to the hash checksum.update(pathBytes, 0, pathBytes.length); entry.getKey().updateChecksum(checksum); }); return checksum; } @UnstableAPI @SneakyThrows(JsonProcessingException.class) final String hash(CreateContainerCmd createCommand) { DefaultDockerClientConfig dockerClientConfig = DefaultDockerClientConfig.createDefaultConfigBuilder().build(); byte[] commandJson = dockerClientConfig .getObjectMapper() .copy() .enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) .enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS) .writeValueAsBytes(createCommand); // TODO add Testcontainers' version to the hash return Hashing.sha1().hashBytes(commandJson).toString(); } @VisibleForTesting Optional findContainerForReuse(String hash) { // TODO locking return dockerClient .listContainersCmd() .withLabelFilter(ImmutableMap.of(HASH_LABEL, hash)) .withLimit(1) .withStatusFilter(Arrays.asList("running")) .exec() .stream() .findAny() .map(it -> it.getId()); } /** * Set any custom settings for the create command such as shared memory size. */ private HostConfig buildHostConfig(HostConfig config) { if (shmSize != null) { config.withShmSize(shmSize); } if (tmpFsMapping != null) { config.withTmpFs(tmpFsMapping); } return config; } private void connectToPortForwardingNetwork(String networkMode) { PortForwardingContainer.INSTANCE .getNetwork() .map(ContainerNetwork::getNetworkID) .ifPresent(networkId -> { if (!Arrays.asList(networkId, "none", "host").contains(networkMode)) { 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(); } } }); } /** * Kill and remove the container. */ @Override public void stop() { if (containerId == null) { return; } try { String imageName; try { imageName = getDockerImageName(); } catch (Exception e) { imageName = ""; } containerIsStopping(containerInfo); ResourceReaper.instance().stopAndRemoveContainer(containerId, imageName); containerIsStopped(containerInfo); } finally { containerId = null; containerInfo = null; } } /** * Provide a logger that references the docker image name. * * @return a logger that references the docker image name */ protected Logger logger() { return DockerLoggerFactory.getLogger(this.getDockerImageName()); } /** * Creates a directory on the local filesystem which will be mounted as a volume for the container. * * @param temporary is the volume directory temporary? If true, the directory will be deleted on JVM shutdown. * @return path to the volume directory */ @Deprecated protected Path createVolumeDirectory(boolean temporary) { Path directory = new File(".tmp-volume-" + UUID.randomUUID()).toPath(); PathUtils.mkdirp(directory); if (temporary) { Runtime .getRuntime() .addShutdownHook( new Thread( DockerClientFactory.TESTCONTAINERS_THREAD_GROUP, () -> { PathUtils.recursiveDeleteDir(directory); } ) ); } return directory; } protected void configure() {} @SuppressWarnings({ "EmptyMethod", "UnusedParameters" }) protected void containerIsCreated(String containerId) {} @SuppressWarnings({ "EmptyMethod", "UnusedParameters" }) protected void containerIsStarting(InspectContainerResponse containerInfo) {} @SuppressWarnings({ "EmptyMethod", "UnusedParameters" }) @UnstableAPI protected void containerIsStarting(InspectContainerResponse containerInfo, boolean reused) { containerIsStarting(containerInfo); } @SuppressWarnings({ "EmptyMethod", "UnusedParameters" }) protected void containerIsStarted(InspectContainerResponse containerInfo) {} @SuppressWarnings({ "EmptyMethod", "UnusedParameters" }) @UnstableAPI protected void containerIsStarted(InspectContainerResponse containerInfo, boolean reused) { containerIsStarted(containerInfo); } /** * A hook that is executed before the container is stopped with {@link #stop()}. * Warning! This hook won't be executed if the container is terminated during * the JVM's shutdown hook or by Ryuk. */ @SuppressWarnings({ "EmptyMethod", "UnusedParameters" }) protected void containerIsStopping(InspectContainerResponse containerInfo) {} /** * A hook that is executed after the container is stopped with {@link #stop()}. * Warning! This hook won't be executed if the container is terminated during * the JVM's shutdown hook or by Ryuk. */ @SuppressWarnings({ "EmptyMethod", "UnusedParameters" }) protected void containerIsStopped(InspectContainerResponse containerInfo) {} /** * @return the port on which to check if the container is ready * @deprecated see {@link GenericContainer#getLivenessCheckPorts()} for replacement */ @Deprecated protected Integer getLivenessCheckPort() { // legacy implementation for backwards compatibility Iterator exposedPortsIterator = this.containerDef.getExposedPorts().iterator(); if (exposedPortsIterator.hasNext()) { return getMappedPort(exposedPortsIterator.next().getPort()); } else if (!this.containerDef.getPortBindings().isEmpty()) { return Integer.valueOf( this.containerDef.getPortBindings().iterator().next().getBinding().getHostPortSpec() ); } else { return null; } } /** * @return the ports on which to check if the container is ready * @deprecated use {@link #getLivenessCheckPortNumbers()} instead */ @NotNull @NonNull @Deprecated protected Set getLivenessCheckPorts() { final Set result = WaitStrategyTarget.super.getLivenessCheckPortNumbers(); // for backwards compatibility if (this.getLivenessCheckPort() != null) { result.add(this.getLivenessCheckPort()); } return result; } @Override public Set getLivenessCheckPortNumbers() { return this.getLivenessCheckPorts(); } private void applyConfiguration(CreateContainerCmd createCommand) { this.containerDef.applyTo(createCommand); buildHostConfig(createCommand.getHostConfig()); VolumesFrom[] volumesFromsArray = volumesFroms.stream().toArray(VolumesFrom[]::new); createCommand.withVolumesFrom(volumesFromsArray); Set allLinks = new HashSet<>(); Set allLinkedContainerNetworks = new HashSet<>(); for (Entry linkEntries : linkedContainers.entrySet()) { String alias = linkEntries.getKey(); LinkableContainer linkableContainer = linkEntries.getValue(); Set links = findLinksFromThisContainer(alias, linkableContainer); allLinks.addAll(links); if (allLinks.size() == 0) { throw new ContainerLaunchException( "Aborting attempt to link to container " + linkableContainer.getContainerName() + " as it is not running" ); } Set linkedContainerNetworks = findAllNetworksForLinkedContainers(linkableContainer); allLinkedContainerNetworks.addAll(linkedContainerNetworks); } createCommand.withLinks(allLinks.toArray(new Link[allLinks.size()])); allLinkedContainerNetworks.remove("bridge"); if (allLinkedContainerNetworks.size() > 1) { logger() .warn( "Container needs to be on more than one custom network to link to other " + "containers - this is not currently supported. Required networks are: {}", allLinkedContainerNetworks ); } Optional networkForLinks = allLinkedContainerNetworks.stream().findFirst(); if (networkForLinks.isPresent()) { logger().debug("Associating container with network: {}", networkForLinks.get()); createCommand.withNetworkMode(networkForLinks.get()); } if (hostAccessible) { PortForwardingContainer.INSTANCE.start(); } PortForwardingContainer.INSTANCE .getNetwork() .ifPresent(it -> { withExtraHost(INTERNAL_HOST_HOSTNAME, it.getIpAddress()); }); String[] extraHostsArray = extraHosts.stream().distinct().toArray(String[]::new); createCommand.withExtraHosts(extraHostsArray); if (workingDirectory != null) { createCommand.withWorkingDir(workingDirectory); } for (CreateContainerCmdModifier createContainerCmdModifier : this.createContainerCmdModifiers) { createCommand = createContainerCmdModifier.modify(createCommand); } } private Set findLinksFromThisContainer(String alias, LinkableContainer linkableContainer) { return dockerClient .listContainersCmd() .withStatusFilter(Arrays.asList("running")) .exec() .stream() .flatMap(container -> Stream.of(container.getNames())) .filter(name -> name.endsWith(linkableContainer.getContainerName())) .map(name -> new Link(name, alias)) .collect(Collectors.toSet()); } private Set findAllNetworksForLinkedContainers(LinkableContainer linkableContainer) { return dockerClient .listContainersCmd() .exec() .stream() .filter(container -> container.getNames()[0].endsWith(linkableContainer.getContainerName())) .filter(container -> { return container.getNetworkSettings() != null && container.getNetworkSettings().getNetworks() != null; }) .flatMap(container -> container.getNetworkSettings().getNetworks().keySet().stream()) .distinct() .collect(Collectors.toSet()); } /** * {@inheritDoc} */ @Override public SELF waitingFor(@NonNull WaitStrategy waitStrategy) { this.waitStrategy = waitStrategy; return self(); } /** * The {@link WaitStrategy} to use to determine if the container is ready. * Defaults to {@link Wait#defaultWaitStrategy()}. * * @return the {@link WaitStrategy} to use */ protected WaitStrategy getWaitStrategy() { return this.waitStrategy == DEFAULT_WAIT_STRATEGY ? this.containerDef.getWaitStrategy() : this.waitStrategy; } @Override public void setWaitStrategy(WaitStrategy waitStrategy) { this.waitStrategy = waitStrategy; } /** * Wait until the container has started. The default implementation simply * waits for a port to start listening; other implementations are available * as implementations of {@link WaitStrategy} * * @see #waitingFor(WaitStrategy) */ protected void waitUntilContainerStarted() { WaitStrategy waitStrategy = getWaitStrategy(); if (waitStrategy != null) { waitStrategy.waitUntilReady(this); } } private String checkForEmulation() { try { DockerClient dockerClient = DockerClientFactory.instance().client(); String imageId = getContainerInfo().getImageId(); String imageArch = dockerClient.inspectImageCmd(imageId).exec().getArch(); String serverArch = dockerClient.versionCmd().exec().getArch(); if (!serverArch.equals(imageArch)) { return ( "The architecture '" + imageArch + "' for image '" + getDockerImageName() + "' (ID " + imageId + ") does not match the Docker server architecture '" + serverArch + "'. This will cause the container to execute much more slowly due to emulation and may lead to timeout failures." ); } } catch (Exception archCheckException) { // ignore any exceptions since this is just used for a log message } return null; } /** * {@inheritDoc} */ @Override public void setCommand(@NonNull String command) { this.containerDef.setCommand(command.split(" ")); } /** * {@inheritDoc} */ @Override public void setCommand(@NonNull String... commandParts) { this.containerDef.setCommand(commandParts); } @Override public Map getEnvMap() { return this.containerDef.envVars; } /** * {@inheritDoc} */ @Override public List getEnv() { return this.containerDef.getEnvVars() .entrySet() .stream() .map(it -> it.getKey() + "=" + it.getValue()) .collect(Collectors.toList()); } @Override public void setEnv(List env) { this.containerDef.setEnvVars( env.stream().map(it -> it.split("=")).collect(Collectors.toMap(it -> it[0], it -> it[1])) ); } /** * {@inheritDoc} */ @Override public void addEnv(String key, String value) { this.containerDef.addEnvVar(key, value); } /** * {@inheritDoc} */ @Override public void addFileSystemBind( final String hostPath, final String containerPath, final BindMode mode, final SelinuxContext selinuxContext ) { if (SystemUtils.IS_OS_WINDOWS && hostPath.startsWith("/")) { // e.g. Docker socket mount this.containerDef.addBinds( new Bind(hostPath, new Volume(containerPath), mode.accessMode, selinuxContext.selContext) ); } else { final MountableFile mountableFile = MountableFile.forHostPath(hostPath); this.containerDef.addBinds( new Bind( mountableFile.getResolvedPath(), new Volume(containerPath), mode.accessMode, selinuxContext.selContext ) ); } } /** * {@inheritDoc} */ @Override public SELF withFileSystemBind(String hostPath, String containerPath, BindMode mode) { addFileSystemBind(hostPath, containerPath, mode); return self(); } /** * {@inheritDoc} */ @Override public SELF withVolumesFrom(Container container, BindMode mode) { addVolumesFrom(container, mode); return self(); } private void addVolumesFrom(Container container, BindMode mode) { volumesFroms.add(new VolumesFrom(container.getContainerName(), mode.accessMode)); } /** * @deprecated Links are deprecated (see #465). Please use {@link Network} features instead. */ @Deprecated @Override public void addLink(LinkableContainer otherContainer, String alias) { this.linkedContainers.put(alias, otherContainer); } @Override public void addExposedPort(Integer port) { this.containerDef.addExposedTcpPort(port); } @Override public void addExposedPorts(int... ports) { this.containerDef.addExposedTcpPorts(ports); } /** * {@inheritDoc} */ @Override public SELF withExposedPorts(Integer... ports) { this.setExposedPorts(Lists.newArrayList(ports)); return self(); } /** * Add a TCP container port that should be bound to a fixed port on the docker host. *

* Note that this method is protected scope to discourage use, as clashes or instability are more likely when * using fixed port mappings. If you need to use this method from a test, please use {@link FixedHostPortGenericContainer} * instead of GenericContainer. * * @param hostPort * @param containerPort */ protected void addFixedExposedPort(int hostPort, int containerPort) { addFixedExposedPort(hostPort, containerPort, InternetProtocol.TCP); } /** * Add a container port that should be bound to a fixed port on the docker host. *

* Note that this method is protected scope to discourage use, as clashes or instability are more likely when * using fixed port mappings. If you need to use this method from a test, please use {@link FixedHostPortGenericContainer} * instead of GenericContainer. * * @param hostPort * @param containerPort * @param protocol */ protected void addFixedExposedPort(int hostPort, int containerPort, InternetProtocol protocol) { ExposedPort exposedPort = new ExposedPort( containerPort, com.github.dockerjava.api.model.InternetProtocol.parse(protocol.name()) ); PortBinding portBinding = new PortBinding(Ports.Binding.bindPort(hostPort), exposedPort); this.containerDef.addPortBindings(portBinding); } /** * {@inheritDoc} */ @Override public SELF withEnv(String key, String value) { this.addEnv(key, value); return self(); } /** * {@inheritDoc} */ @Override public SELF withEnv(Map env) { env.forEach(this.containerDef::addEnvVar); return self(); } /** * {@inheritDoc} */ @Override public SELF withLabel(String key, String value) { if (key.startsWith("org.testcontainers")) { throw new IllegalArgumentException("The org.testcontainers namespace is reserved for interal use"); } this.containerDef.addLabel(key, value); return self(); } /** * {@inheritDoc} */ @Override public SELF withLabels(Map labels) { labels.forEach(this::withLabel); return self(); } /** * {@inheritDoc} */ @Override public SELF withCommand(String cmd) { this.setCommand(cmd); return self(); } /** * {@inheritDoc} */ @Override public SELF withCommand(String... commandParts) { this.setCommand(commandParts); return self(); } /** * {@inheritDoc} */ @Override public SELF withExtraHost(String hostname, String ipAddress) { this.extraHosts.add(String.format("%s:%s", hostname, ipAddress)); return self(); } @Override public SELF withNetworkMode(String networkMode) { this.containerDef.setNetworkMode(networkMode); return self(); } @Override public SELF withNetwork(Network network) { this.containerDef.setNetwork(network); return self(); } @Override public SELF withNetworkAliases(String... aliases) { this.containerDef.addNetworkAliases(aliases); return self(); } @Override public SELF withImagePullPolicy(ImagePullPolicy imagePullPolicy) { this.image = this.image.withImagePullPolicy(imagePullPolicy); return self(); } /** * {@inheritDoc} */ @Override public SELF withClasspathResourceMapping( final String resourcePath, final String containerPath, final BindMode mode ) { return withClasspathResourceMapping(resourcePath, containerPath, mode, SelinuxContext.SHARED); } /** * {@inheritDoc} */ @Override public SELF withClasspathResourceMapping( final String resourcePath, final String containerPath, final BindMode mode, final SelinuxContext selinuxContext ) { final MountableFile mountableFile = MountableFile.forClasspathResource(resourcePath); if (mode == BindMode.READ_WRITE) { addFileSystemBind(mountableFile.getResolvedPath(), containerPath, mode, selinuxContext); } else { withCopyFileToContainer(mountableFile, containerPath); } return self(); } /** * {@inheritDoc} */ @Override public SELF withStartupTimeout(Duration startupTimeout) { getWaitStrategy().withStartupTimeout(startupTimeout); return self(); } @Override public SELF withPrivilegedMode(boolean mode) { this.containerDef.setPrivilegedMode(mode); return self(); } /** * {@inheritDoc} */ @Override public SELF withMinimumRunningDuration(Duration minimumRunningDuration) { this.startupCheckStrategy = new MinimumDurationRunningStartupCheckStrategy(minimumRunningDuration); return self(); } /** * {@inheritDoc} */ @Override public SELF withStartupCheckStrategy(StartupCheckStrategy strategy) { this.startupCheckStrategy = strategy; return self(); } /** * {@inheritDoc} */ @Override public SELF withWorkingDirectory(String workDir) { this.setWorkingDirectory(workDir); return self(); } /** * {@inheritDoc} */ @Override public SELF withCopyFileToContainer(MountableFile mountableFile, String containerPath) { if (copyToFileContainerPathMap.containsKey(mountableFile)) { throw new IllegalStateException("Path already configured for copy: " + mountableFile); } copyToFileContainerPathMap.put(mountableFile, containerPath); return self(); } /** * {@inheritDoc} */ @Override public SELF withCopyToContainer(Transferable transferable, String containerPath) { copyToTransferableContainerPathMap.put(transferable, containerPath); return self(); } /** * Get the IP address that this container may be reached on (may not be the local machine). * * @return an IP address * @deprecated please use getContainerIpAddress() instead */ @Deprecated public String getIpAddress() { return getHost(); } /** * {@inheritDoc} */ @Override public void setDockerImageName(@NonNull String dockerImageName) { this.image = new RemoteDockerImage(dockerImageName); } /** * {@inheritDoc} */ @Override @NonNull public String getDockerImageName() { try { return image.get(); } catch (Exception e) { throw new ContainerFetchException("Can't get Docker image: " + image, e); } } /** * {@inheritDoc} */ @Override @Deprecated public String getTestHostIpAddress() { if (DockerMachineClient.instance().isInstalled()) { try { Optional defaultMachine = DockerMachineClient.instance().getDefaultMachine(); if (!defaultMachine.isPresent()) { throw new IllegalStateException("Could not find a default docker-machine instance"); } String sshConnectionString = CommandLine .runShellCommand("docker-machine", "ssh", defaultMachine.get(), "echo $SSH_CONNECTION") .trim(); if (Strings.isNullOrEmpty(sshConnectionString)) { throw new IllegalStateException( "Could not obtain SSH_CONNECTION environment variable for docker machine " + defaultMachine.get() ); } String[] sshConnectionParts = sshConnectionString.split("\\s"); if (sshConnectionParts.length != 4) { throw new IllegalStateException( "Unexpected pattern for SSH_CONNECTION for docker machine - expected 'IP PORT IP PORT' pattern but found '" + sshConnectionString + "'" ); } return sshConnectionParts[0]; } catch (Exception e) { throw new RuntimeException(e); } } else { throw new UnsupportedOperationException( "getTestHostIpAddress() is only implemented for docker-machine right now" ); } } /** * {@inheritDoc} */ @Override public SELF withLogConsumer(Consumer consumer) { this.logConsumers.add(consumer); return self(); } /** * {@inheritDoc} */ @Override @SneakyThrows public void copyFileFromContainer(String containerPath, String destinationPath) { Container.super.copyFileFromContainer(containerPath, destinationPath); } /** * Allow container startup to be attempted more than once if an error occurs. To be used if containers are * 'flaky' but this flakiness is not something that should affect test outcomes. * * @param attempts number of attempts */ public SELF withStartupAttempts(int attempts) { this.startupAttempts = attempts; return self(); } /** * Allow low level modifications of {@link CreateContainerCmd} after it was pre-configured in {@link #tryStart()}. * Invocation happens eagerly on a moment when container is created. * Warning: this does expose the underlying docker-java API so might change outside of our control. * * @param modifier {@link Consumer} of {@link CreateContainerCmd}. * @return this */ public SELF withCreateContainerCmdModifier(Consumer modifier) { this.createContainerCmdModifiers.add(cmd -> { modifier.accept(cmd); return cmd; }); return self(); } /** * Size of /dev/shm * * @param bytes The number of bytes to assign the shared memory. If null, it will apply the Docker default which is 64 MB. * @return this */ public SELF withSharedMemorySize(Long bytes) { this.shmSize = bytes; return self(); } /** * First class support for configuring tmpfs * * @param mapping path and params of tmpfs/mount flag for container * @return this */ public SELF withTmpFs(Map mapping) { this.tmpFsMapping = mapping; return self(); } @UnstableAPI public SELF withReuse(boolean reusable) { this.shouldBeReused = reusable; return self(); } /** * Forces access to the tests host machine. * Use this method if you need to call {@link org.testcontainers.Testcontainers#exposeHostPorts(int...)} * after you start this container. * * @return this */ public SELF withAccessToHost(boolean value) { this.hostAccessible = value; return self(); } @Override public boolean equals(Object o) { return this == o; } @Override public int hashCode() { return System.identityHashCode(this); } @Override public String getContainerName() { return getContainerInfo().getName(); } public Network getNetwork() { return this.containerDef.getNetwork(); } @Override public List getBinds() { return this.containerDef.binds; } @Override public void setBinds(List binds) { this.containerDef.setBinds(binds); } @Override public String[] getCommandParts() { return this.containerDef.getCommand(); } @Override public void setCommandParts(String[] commandParts) { this.containerDef.setCommand(commandParts); } public List getNetworkAliases() { return new ArrayList<>(this.containerDef.getNetworkAliases()); } public void setNetworkAliases(List aliases) { this.containerDef.setNetworkAliases(new HashSet<>(aliases)); } @Override public List getPortBindings() { return this.containerDef.portBindings.stream() .map(it -> String.format("%s:%s", it.getBinding(), it.getExposedPort())) .collect(Collectors.toList()); } @Override public void setPortBindings(List portBindings) { this.containerDef.setPortBindings(portBindings.stream().map(PortBinding::parse).collect(Collectors.toSet())); } public void setPrivilegedMode(boolean mode) { this.containerDef.setPrivilegedMode(mode); } public boolean isPrivilegedMode() { return this.containerDef.isPrivilegedMode(); } public Map getLabels() { return this.containerDef.labels; } public void setLabels(Map labels) { this.containerDef.setLabels(labels); } public String getNetworkMode() { return this.containerDef.getNetworkMode(); } public void setNetworkMode(String networkMode) { this.containerDef.setNetworkMode(networkMode); } public void setNetwork(Network network) { this.containerDef.setNetwork(network); } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/InternetProtocol.java ================================================ package org.testcontainers.containers; /** * The IP protocols supported by Docker. */ public enum InternetProtocol { TCP, UDP; public String toDockerNotation() { return name().toLowerCase(); } public static InternetProtocol fromDockerNotation(String protocol) { return valueOf(protocol.toUpperCase()); } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/LocalDockerCompose.java ================================================ package org.testcontainers.containers; import com.github.dockerjava.core.LocalDirectorySSLConfig; import com.github.dockerjava.transport.SSLConfig; import com.google.common.base.Splitter; import com.google.common.collect.Maps; import org.slf4j.Logger; import org.testcontainers.DockerClientFactory; import org.testcontainers.dockerclient.TransportConfig; import org.testcontainers.utility.CommandLine; import org.testcontainers.utility.DockerLoggerFactory; import org.zeroturnaround.exec.InvalidExitValueException; import org.zeroturnaround.exec.ProcessExecutor; import org.zeroturnaround.exec.stream.slf4j.Slf4jStream; import java.io.File; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; /** * Use local Docker Compose binary, if present. */ class LocalDockerCompose implements DockerCompose { private final List composeFiles; private final String identifier; private String cmd = ""; private Map env = new HashMap<>(); private final String composeExecutable; public LocalDockerCompose(String composeExecutable, List composeFiles, String identifier) { this.composeExecutable = composeExecutable; this.composeFiles = composeFiles; this.identifier = identifier; } @Override public DockerCompose withCommand(String cmd) { this.cmd = cmd; return this; } @Override public DockerCompose withEnv(Map env) { this.env = env; return this; } @Override public void invoke() { // bail out early if (!CommandLine.executableExists(this.composeExecutable)) { throw new ContainerLaunchException( "Local Docker Compose not found. Is " + this.composeExecutable + " on the PATH?" ); } final Map environment = Maps.newHashMap(env); environment.put(ENV_PROJECT_NAME, identifier); TransportConfig transportConfig = DockerClientFactory.instance().getTransportConfig(); SSLConfig sslConfig = transportConfig.getSslConfig(); if (sslConfig != null) { if (sslConfig instanceof LocalDirectorySSLConfig) { environment.put("DOCKER_CERT_PATH", ((LocalDirectorySSLConfig) sslConfig).getDockerCertPath()); environment.put("DOCKER_TLS_VERIFY", "true"); } else { logger() .warn( "Couldn't set DOCKER_CERT_PATH. `sslConfig` is present but it's not LocalDirectorySSLConfig." ); } } String dockerHost = transportConfig.getDockerHost().toString(); environment.put("DOCKER_HOST", dockerHost); final Stream absoluteDockerComposeFilePaths = composeFiles .stream() .map(File::getAbsolutePath) .map(Objects::toString); final String composeFileEnvVariableValue = absoluteDockerComposeFilePaths.collect( Collectors.joining(File.pathSeparator + "") ); logger().debug("Set env COMPOSE_FILE={}", composeFileEnvVariableValue); final File pwd = composeFiles.get(0).getAbsoluteFile().getParentFile().getAbsoluteFile(); environment.put(ENV_COMPOSE_FILE, composeFileEnvVariableValue); logger().info("Local Docker Compose is running command: {}", cmd); final List command = Splitter .onPattern(" ") .omitEmptyStrings() .splitToList(this.composeExecutable + " " + cmd); try { new ProcessExecutor() .command(command) .redirectOutput(Slf4jStream.of(logger()).asInfo()) .redirectError(Slf4jStream.of(logger()).asInfo()) // docker-compose will log pull information to stderr .environment(environment) .directory(pwd) .exitValueNormal() .executeNoTimeout(); logger().info("Docker Compose has finished running"); } catch (InvalidExitValueException e) { throw new ContainerLaunchException( "Local Docker Compose exited abnormally with code " + e.getExitValue() + " whilst running command: " + cmd ); } catch (Exception e) { throw new ContainerLaunchException("Error running local Docker Compose command: " + cmd, e); } } /** * @return a logger */ private Logger logger() { return DockerLoggerFactory.getLogger(this.composeExecutable); } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/Network.java ================================================ package org.testcontainers.containers; import com.github.dockerjava.api.command.CreateNetworkCmd; import lombok.Builder; import lombok.Getter; import lombok.Singular; import org.testcontainers.DockerClientFactory; import org.testcontainers.utility.ResourceReaper; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; public interface Network extends AutoCloseable { Network SHARED = new NetworkImpl(false, null, Collections.emptySet(), null) { @Override public void close() { // Do not allow users to close SHARED network, only ResourceReaper is allowed to close (destroy) it } }; String getId(); @Override void close(); static Network newNetwork() { return builder().build(); } static NetworkImpl.NetworkImplBuilder builder() { return NetworkImpl.builder(); } @Builder @Getter class NetworkImpl implements Network { private final String name = UUID.randomUUID().toString(); private Boolean enableIpv6; private String driver; @Singular private Set> createNetworkCmdModifiers; @Deprecated private String id; private final AtomicBoolean initialized = new AtomicBoolean(); @Override public synchronized String getId() { if (initialized.compareAndSet(false, true)) { boolean success = false; try { id = create(); success = true; } finally { if (!success) { initialized.set(false); } } } return id; } private String create() { CreateNetworkCmd createNetworkCmd = DockerClientFactory.instance().client().createNetworkCmd(); createNetworkCmd.withName(name); createNetworkCmd.withCheckDuplicate(true); if (enableIpv6 != null) { createNetworkCmd.withEnableIpv6(enableIpv6); } if (driver != null) { createNetworkCmd.withDriver(driver); } for (Consumer consumer : createNetworkCmdModifiers) { consumer.accept(createNetworkCmd); } Map labels = createNetworkCmd.getLabels(); labels = new HashMap<>(labels != null ? labels : Collections.emptyMap()); labels.putAll(DockerClientFactory.DEFAULT_LABELS); //noinspection deprecation labels.putAll(ResourceReaper.instance().getLabels()); createNetworkCmd.withLabels(labels); return createNetworkCmd.exec().getId(); } @Override public synchronized void close() { if (initialized.getAndSet(false)) { ResourceReaper.instance().removeNetworkById(id); } } } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java ================================================ package org.testcontainers.containers; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Sets; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; import org.testcontainers.images.ParsedDockerfile; import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; import org.yaml.snakeyaml.nodes.Node; import org.yaml.snakeyaml.nodes.Tag; import org.yaml.snakeyaml.representer.Representer; import org.yaml.snakeyaml.resolver.Resolver; import java.io.File; import java.io.FileInputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import java.util.Set; /** * Representation of a docker-compose file, with partial parsing for validation and extraction of a minimal set of * data. */ @Slf4j @EqualsAndHashCode class ParsedDockerComposeFile { private final Map composeFileContent; private final String composeFileName; private final File composeFile; @Getter private final Map> serviceNameToImageNames = new HashMap<>(); ParsedDockerComposeFile(File composeFile) { // The default is 50 and a big docker-compose.yml file can easily go above that number. 1,000 should give us some room LoaderOptions options = new LoaderOptions(); options.setMaxAliasesForCollections(1_000); DumperOptions dumperOptions = new DumperOptions(); SafeConstructor constructor = new SafeConstructor(options) { @Override protected Object constructObject(Node node) { if (node.getTag().equals(new Tag("!reset"))) { return null; } return super.constructObject(node); } }; Yaml yaml = new Yaml(constructor, new Representer(dumperOptions), dumperOptions, options, new Resolver()); try (FileInputStream fileInputStream = FileUtils.openInputStream(composeFile)) { composeFileContent = yaml.load(fileInputStream); } catch (Exception e) { throw new IllegalArgumentException("Unable to parse YAML file from " + composeFile.getAbsolutePath(), e); } this.composeFileName = composeFile.getAbsolutePath(); this.composeFile = composeFile; parseAndValidate(); } @VisibleForTesting ParsedDockerComposeFile(Map testContent) { this.composeFileContent = testContent; this.composeFileName = ""; this.composeFile = new File("."); parseAndValidate(); } private void parseAndValidate() { final Map servicesMap; 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( "Compose file {} has an unknown format: 'version' is set but 'services' is not defined", composeFileName ); return; } if (!(servicesElement instanceof Map)) { log.debug("Compose file {} has an unknown format: 'services' is not Map", composeFileName); return; } @SuppressWarnings("unchecked") Map temp = (Map) servicesElement; servicesMap = temp; } else { servicesMap = composeFileContent; } for (Map.Entry entry : servicesMap.entrySet()) { String serviceName = entry.getKey(); Object serviceDefinition = entry.getValue(); if (!(serviceDefinition instanceof Map)) { log.debug( "Compose file {} has an unknown format: service '{}' is not Map", composeFileName, serviceName ); break; } @SuppressWarnings("unchecked") final Map serviceDefinitionMap = (Map) serviceDefinition; validateNoContainerNameSpecified(serviceName, serviceDefinitionMap); findServiceImageName(serviceName, serviceDefinitionMap); findImageNamesInDockerfile(serviceName, serviceDefinitionMap); } } private void validateNoContainerNameSpecified(String serviceName, Map serviceDefinitionMap) { if (serviceDefinitionMap.containsKey("container_name")) { throw new IllegalStateException( String.format( "Compose file %s has 'container_name' property set for service '%s' but this property is not supported by Testcontainers, consider removing it", composeFileName, serviceName ) ); } } private void findServiceImageName(String serviceName, Map serviceDefinitionMap) { Object result = serviceDefinitionMap.get("image"); if (result instanceof String) { final String imageName = (String) result; log.debug("Resolved dependency image for Docker Compose in {}: {}", composeFileName, imageName); serviceNameToImageNames.put(serviceName, Sets.newHashSet(imageName)); } } private void findImageNamesInDockerfile(String serviceName, Map serviceDefinitionMap) { final Object buildNode = serviceDefinitionMap.get("build"); Path dockerfilePath = null; if (buildNode instanceof Map) { final Map buildElement = (Map) buildNode; final Object dockerfileRelativePath = buildElement.get("dockerfile"); final Object contextRelativePath = buildElement.get("context"); if (dockerfileRelativePath instanceof String && contextRelativePath instanceof String) { dockerfilePath = composeFile .getAbsoluteFile() .getParentFile() .toPath() .resolve((String) contextRelativePath) .resolve((String) dockerfileRelativePath) .normalize(); } } else if (buildNode instanceof String) { dockerfilePath = composeFile .getAbsoluteFile() .getParentFile() .toPath() .resolve((String) buildNode) .resolve("./Dockerfile") .normalize(); } if (dockerfilePath != null && Files.exists(dockerfilePath)) { Set resolvedImageNames = new ParsedDockerfile(dockerfilePath).getDependencyImageNames(); if (!resolvedImageNames.isEmpty()) { log.debug( "Resolved Dockerfile dependency images for Docker Compose in {} -> {}: {}", composeFileName, dockerfilePath, resolvedImageNames ); this.serviceNameToImageNames.put(serviceName, resolvedImageNames); } } } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/PortForwardingContainer.java ================================================ package org.testcontainers.containers; import com.github.dockerjava.api.model.ContainerNetwork; import com.trilead.ssh2.Connection; import lombok.AccessLevel; import lombok.Getter; import lombok.SneakyThrows; import org.testcontainers.utility.DockerImageName; import java.time.Duration; import java.util.AbstractMap; import java.util.Collections; import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; public enum PortForwardingContainer { INSTANCE; private static String PASSWORD = UUID.randomUUID().toString(); private static ContainerDef DEFINITION = new ContainerDef() { { setImage(DockerImageName.parse("testcontainers/sshd:1.3.0")); addExposedTcpPort(22); addEnvVar("PASSWORD", PASSWORD); } }; private GenericContainer container; private final Set> exposedPorts = Collections.newSetFromMap(new ConcurrentHashMap<>()); @Getter(value = AccessLevel.PRIVATE, lazy = true) private final Connection sshConnection = createSSHSession(); @SneakyThrows private Connection createSSHSession() { container = new GenericContainer<>(DEFINITION); container.start(); Connection connection = new Connection(container.getHost(), container.getMappedPort(22)); connection.setTCPNoDelay(true); connection.connect( (hostname, port, serverHostKeyAlgorithm, serverHostKey) -> true, (int) Duration.ofSeconds(30).toMillis(), (int) Duration.ofSeconds(30).toMillis() ); if (!connection.authenticateWithPassword("root", PASSWORD)) { throw new IllegalStateException("Authentication failed."); } return connection; } @SneakyThrows public void exposeHostPort(int port) { exposeHostPort(port, port); } @SneakyThrows public void exposeHostPort(int hostPort, int containerPort) { if (exposedPorts.add(new AbstractMap.SimpleEntry<>(hostPort, containerPort))) { getSshConnection().requestRemotePortForwarding("", containerPort, "localhost", hostPort); } } void start() { getSshConnection(); } Optional getNetwork() { return Optional .ofNullable(container) .map(GenericContainer::getContainerInfo) .flatMap(it -> it.getNetworkSettings().getNetworks().values().stream().findFirst()); } void reset() { if (container != null) { container.stop(); } container = null; ((AtomicReference) (Object) sshConnection).set(null); exposedPorts.clear(); } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/SelinuxContext.java ================================================ package org.testcontainers.containers; import com.github.dockerjava.api.model.SELContext; import lombok.AllArgsConstructor; /** * Possible contexts for use with SELinux */ @AllArgsConstructor public enum SelinuxContext { SHARED(SELContext.shared), SINGLE(SELContext.single), NONE(SELContext.none); public final SELContext selContext; } ================================================ FILE: core/src/main/java/org/testcontainers/containers/SocatContainer.java ================================================ package org.testcontainers.containers; import org.testcontainers.utility.Base58; import org.testcontainers.utility.DockerImageName; import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; /** * A socat container is used as a TCP proxy, enabling any TCP port of another container to be exposed * publicly, even if that container does not make the port public itself. */ public class SocatContainer extends GenericContainer { private final Map targets = new HashMap<>(); public SocatContainer() { this(DockerImageName.parse("alpine/socat:1.7.4.3-r0")); } public SocatContainer(final DockerImageName dockerImageName) { super(dockerImageName); withCreateContainerCmdModifier(it -> it.withEntrypoint("/bin/sh")); withCreateContainerCmdModifier(it -> it.withName("testcontainers-socat-" + Base58.randomString(8))); } public SocatContainer withTarget(int exposedPort, String host) { return withTarget(exposedPort, host, exposedPort); } public SocatContainer withTarget(int exposedPort, String host, int internalPort) { addExposedPort(exposedPort); targets.put(exposedPort, String.format("%s:%s", host, internalPort)); return self(); } @Override protected void configure() { withCommand( "-c", targets .entrySet() .stream() .map(entry -> "socat TCP-LISTEN:" + entry.getKey() + ",fork,reuseaddr TCP:" + entry.getValue()) .collect(Collectors.joining(" & ")) ); } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/VncRecordingContainer.java ================================================ package org.testcontainers.containers; import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.ToString; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; import org.testcontainers.utility.DockerImageName; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.Base64; /** * 'Sidekick container' with the sole purpose of recording the VNC screen output from another container. * */ @Getter @ToString public class VncRecordingContainer extends GenericContainer { private static final String ORIGINAL_RECORDING_FILE_NAME = "/screen.flv"; public static final String DEFAULT_VNC_PASSWORD = "secret"; public static final int DEFAULT_VNC_PORT = 5900; static final VncRecordingFormat DEFAULT_RECORDING_FORMAT = VncRecordingFormat.FLV; private final String targetNetworkAlias; private String vncPassword = DEFAULT_VNC_PASSWORD; private VncRecordingFormat videoFormat = DEFAULT_RECORDING_FORMAT; private int vncPort = 5900; private int frameRate = 30; public VncRecordingContainer(@NonNull GenericContainer targetContainer) { this( targetContainer.getNetwork(), targetContainer .getNetworkAliases() .stream() .findFirst() .orElseThrow(() -> new IllegalStateException("Target container must have a network alias")) ); } /** * Create a sidekick container and attach it to another container. The VNC output of that container will be recorded. */ public VncRecordingContainer(@NonNull Network network, @NonNull String targetNetworkAlias) throws IllegalStateException { super(DockerImageName.parse("testcontainers/vnc-recorder:1.3.0")); this.targetNetworkAlias = targetNetworkAlias; withNetwork(network); waitingFor( new LogMessageWaitStrategy() .withRegEx(".*Connected.*") .withStartupTimeout(Duration.of(15, ChronoUnit.SECONDS)) ); } public VncRecordingContainer withVncPassword(@NonNull String vncPassword) { this.vncPassword = vncPassword; return this; } public VncRecordingContainer withVncPort(int vncPort) { this.vncPort = vncPort; return this; } public VncRecordingContainer withVideoFormat(VncRecordingFormat videoFormat) { if (videoFormat != null) { this.videoFormat = videoFormat; } return this; } public VncRecordingContainer withFrameRate(int frameRate) { this.frameRate = frameRate; return this; } @Override protected void configure() { withCreateContainerCmdModifier(it -> it.withEntrypoint("/bin/sh")); String encodedPassword = Base64.getEncoder().encodeToString(vncPassword.getBytes()); setCommand( "-c", "echo '" + encodedPassword + "' | base64 -d > /vnc_password && " + "flvrec.py -o " + ORIGINAL_RECORDING_FILE_NAME + " -d -r " + frameRate + " -P /vnc_password " + targetNetworkAlias + " " + vncPort ); } @SneakyThrows public InputStream streamRecording() { String newRecordingFileName = videoFormat.reencodeRecording(this, ORIGINAL_RECORDING_FILE_NAME); TarArchiveInputStream archiveInputStream = new TarArchiveInputStream( dockerClient.copyArchiveFromContainerCmd(getContainerId(), newRecordingFileName).exec() ); archiveInputStream.getNextEntry(); return archiveInputStream; } @SneakyThrows public void saveRecordingToFile(@NonNull File file) { try (InputStream inputStream = streamRecording()) { Files.copy(inputStream, file.toPath(), StandardCopyOption.REPLACE_EXISTING); } } @RequiredArgsConstructor public enum VncRecordingFormat { FLV("flv") { @Override String reencodeRecording(@NonNull VncRecordingContainer container, @NonNull String source) throws IOException, InterruptedException { String newFileOutput = "/newScreen.flv"; container.execInContainer("ffmpeg", "-i", source, "-vcodec", "libx264", newFileOutput); return newFileOutput; } }, MP4("mp4") { @Override String reencodeRecording(@NonNull VncRecordingContainer container, @NonNull String source) throws IOException, InterruptedException { String newFileOutput = "/newScreen.mp4"; container.execInContainer( "ffmpeg", "-i", source, "-vcodec", "libx264", "-movflags", "faststart", "-pix_fmt", "yuv420p", newFileOutput ); return newFileOutput; } }; abstract String reencodeRecording(VncRecordingContainer container, String source) throws IOException, InterruptedException; @Getter private final String filenameExtension; } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/output/BaseConsumer.java ================================================ package org.testcontainers.containers.output; import lombok.Getter; import lombok.Setter; import java.util.function.Consumer; public abstract class BaseConsumer> implements Consumer { @Getter @Setter private boolean removeColorCodes = true; public SELF withRemoveAnsiCodes(boolean removeAnsiCodes) { this.removeColorCodes = removeAnsiCodes; return (SELF) this; } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/output/FrameConsumerResultCallback.java ================================================ package org.testcontainers.containers.output; import com.github.dockerjava.api.async.ResultCallbackTemplate; import com.github.dockerjava.api.model.Frame; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.function.Consumer; import java.util.regex.Pattern; /** * This class can be used as a generic callback for docker-java commands that produce Frames. */ public class FrameConsumerResultCallback extends ResultCallbackTemplate { private static final Logger LOGGER = LoggerFactory.getLogger(FrameConsumerResultCallback.class); private final Map consumers = new HashMap<>(); private final CountDownLatch completionLatch = new CountDownLatch(1); /** * Set this callback to use the specified consumer for the given output type. * The same consumer can be configured for more than one output type. * @param outputType the output type to configure * @param consumer the consumer to use for that output type */ public void addConsumer(OutputFrame.OutputType outputType, Consumer consumer) { consumers.put(outputType, new LineConsumer(outputType, consumer)); } @Override public void onNext(Frame frame) { if (frame != null) { final OutputFrame.OutputType type = OutputFrame.OutputType.forStreamType(frame.getStreamType()); if (type != null) { final LineConsumer consumer = consumers.get(type); if (consumer == null) { LOGGER.error("got frame with type {}, for which no handler is configured", frame.getStreamType()); } else if (frame.getPayload() != null) { consumer.processFrame(frame.getPayload()); } } } } @Override public void onError(Throwable throwable) { // Sink any errors try { close(); } catch (IOException ignored) {} } @Override public void close() throws IOException { if (completionLatch.getCount() == 0) { return; } consumers.values().forEach(LineConsumer::processBuffer); consumers.values().forEach(LineConsumer::end); super.close(); completionLatch.countDown(); } /** * @return a {@link CountDownLatch} that may be used to wait until {@link #close()} has been called. */ public CountDownLatch getCompletionLatch() { return completionLatch; } private static class LineConsumer { private static final Pattern ANSI_COLOR_PATTERN = Pattern.compile("\u001B\\[[0-9;]+m"); private final OutputFrame.OutputType type; private final Consumer consumer; private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); private boolean lastCR = false; LineConsumer(final OutputFrame.OutputType type, final Consumer consumer) { this.type = type; this.consumer = consumer; } void processFrame(final byte[] b) { int start = 0; int i = 0; while (i < b.length) { switch (b[i]) { case '\n': buffer.write(b, start, i + 1 - start); start = i + 1; consume(); lastCR = false; break; case '\r': if (lastCR) { consume(); } buffer.write(b, start, i + 1 - start); start = i + 1; lastCR = true; break; default: if (lastCR) { consume(); } lastCR = false; } i++; } buffer.write(b, start, b.length - start); } void processBuffer() { if (buffer.size() > 0) { consume(); } } void end() { consumer.accept(OutputFrame.END); } private void consume() { final String string = new String(buffer.toByteArray(), StandardCharsets.UTF_8); final byte[] bytes = processAnsiColorCodes(string).getBytes(StandardCharsets.UTF_8); consumer.accept(new OutputFrame(type, bytes)); buffer.reset(); } private String processAnsiColorCodes(final String utf8String) { if (!(consumer instanceof BaseConsumer) || ((BaseConsumer) consumer).isRemoveColorCodes()) { return ANSI_COLOR_PATTERN.matcher(utf8String).replaceAll(""); } return utf8String; } } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/output/OutputFrame.java ================================================ package org.testcontainers.containers.output; import com.github.dockerjava.api.model.Frame; import com.github.dockerjava.api.model.StreamType; import java.nio.charset.StandardCharsets; /** * Holds exactly one complete line of container output. Lines are split on newline characters (LF, CR LF). */ public class OutputFrame { public static final OutputFrame END = new OutputFrame(OutputType.END, null); private final OutputType type; private final byte[] bytes; public OutputFrame(final OutputType type, final byte[] bytes) { this.type = type; this.bytes = bytes; } public OutputType getType() { return type; } public byte[] getBytes() { return bytes; } public String getUtf8String() { return (bytes == null) ? "" : new String(bytes, StandardCharsets.UTF_8); } public String getUtf8StringWithoutLineEnding() { if (bytes == null) { return ""; } return new String(bytes, 0, bytes.length - determineLineEndingLength(bytes), StandardCharsets.UTF_8); } private static int determineLineEndingLength(final byte[] bytes) { if (bytes.length > 0) { switch (bytes[bytes.length - 1]) { case '\r': return 1; case '\n': return ((bytes.length > 1) && (bytes[bytes.length - 2] == '\r')) ? 2 : 1; } } return 0; } public enum OutputType { STDOUT, STDERR, END; public static OutputType forStreamType(StreamType streamType) { switch (streamType) { case RAW: case STDOUT: return STDOUT; case STDERR: return STDERR; default: return null; } } } public static OutputFrame forFrame(Frame frame) { final OutputType outputType = OutputType.forStreamType(frame.getStreamType()); if (outputType == null) { return null; } return new OutputFrame(outputType, frame.getPayload()); } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/output/Slf4jLogConsumer.java ================================================ package org.testcontainers.containers.output; import org.slf4j.Logger; import org.slf4j.MDC; import java.util.HashMap; import java.util.Map; /** * A consumer for container output that logs output to an SLF4J logger. */ public class Slf4jLogConsumer extends BaseConsumer { private final Logger logger; private final Map mdc = new HashMap<>(); private boolean separateOutputStreams; private String prefix = ""; public Slf4jLogConsumer(Logger logger) { this(logger, false); } public Slf4jLogConsumer(Logger logger, boolean separateOutputStreams) { this.logger = logger; this.separateOutputStreams = separateOutputStreams; } public Slf4jLogConsumer withPrefix(String prefix) { this.prefix = "[" + prefix + "] "; return this; } public Slf4jLogConsumer withMdc(String key, String value) { mdc.put(key, value); return this; } public Slf4jLogConsumer withMdc(Map mdc) { this.mdc.putAll(mdc); return this; } public Slf4jLogConsumer withSeparateOutputStreams() { this.separateOutputStreams = true; return this; } @Override public void accept(OutputFrame outputFrame) { final OutputFrame.OutputType outputType = outputFrame.getType(); final String utf8String = outputFrame.getUtf8StringWithoutLineEnding(); final Map originalMdc = MDC.getCopyOfContextMap(); MDC.setContextMap(mdc); try { switch (outputType) { case END: break; case STDOUT: if (separateOutputStreams) { logger.info("{}{}", prefix.isEmpty() ? "" : (prefix + ": "), utf8String); } else { logger.info("{}{}: {}", prefix, outputType, utf8String); } break; case STDERR: if (separateOutputStreams) { logger.error("{}{}", prefix.isEmpty() ? "" : (prefix + ": "), utf8String); } else { logger.info("{}{}: {}", prefix, outputType, utf8String); } break; default: throw new IllegalArgumentException("Unexpected outputType " + outputType); } } finally { if (originalMdc == null) { MDC.clear(); } else { MDC.setContextMap(originalMdc); } } } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/output/ToStringConsumer.java ================================================ package org.testcontainers.containers.output; import com.google.common.base.Charsets; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.Charset; /** * Created by rnorth on 26/03/2016. */ public class ToStringConsumer extends BaseConsumer { private final ByteArrayOutputStream stringBuffer = new ByteArrayOutputStream(); @Override public void accept(OutputFrame outputFrame) { try { final byte[] bytes = outputFrame.getBytes(); if (bytes != null) { stringBuffer.write(bytes); } } catch (IOException e) { throw new RuntimeException(e); } } public String toUtf8String() { byte[] bytes = stringBuffer.toByteArray(); return new String(bytes, Charsets.UTF_8); } public String toString(Charset charset) { byte[] bytes = stringBuffer.toByteArray(); return new String(bytes, charset); } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/output/WaitingConsumer.java ================================================ package org.testcontainers.containers.output; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Predicate; /** * A consumer for container output that buffers lines in a {@link java.util.concurrent.BlockingDeque} and enables tests * to wait for a matching condition. */ public class WaitingConsumer extends BaseConsumer { private static final Logger LOGGER = LoggerFactory.getLogger(WaitingConsumer.class); private LinkedBlockingDeque frames = new LinkedBlockingDeque<>(); @Override public void accept(OutputFrame frame) { frames.add(frame); } /** * Get access to the underlying frame buffer. Modifying the buffer contents is likely to cause problems if the * waitUntil() methods are also being used, as they feed on the same data. * * @return the collection of frames */ public LinkedBlockingDeque getFrames() { return frames; } /** * Wait until any frame (usually, line) of output matches the provided predicate. *

* Note that lines will often have a trailing newline character, and this is not stripped off before the * predicate is tested. * * @param predicate a predicate to test against each frame */ public void waitUntil(Predicate predicate) throws TimeoutException { // ~2.9 thousands centuries ought to be enough for anyone waitUntil(predicate, Long.MAX_VALUE, 1); } /** * Wait until any frame (usually, line) of output matches the provided predicate. *

* Note that lines will often have a trailing newline character, and this is not stripped off before the * predicate is tested. * * @param predicate a predicate to test against each frame * @param limit maximum time to wait * @param limitUnit maximum time to wait (units) */ public void waitUntil(Predicate predicate, int limit, TimeUnit limitUnit) throws TimeoutException { waitUntil(predicate, limit, limitUnit, 1); } /** * Wait until any frame (usually, line) of output matches the provided predicate. *

* Note that lines will often have a trailing newline character, and this is not stripped off before the * predicate is tested. * * @param predicate a predicate to test against each frame * @param limit maximum time to wait * @param limitUnit maximum time to wait (units) * @param times number of times the predicate has to match */ public void waitUntil(Predicate predicate, long limit, TimeUnit limitUnit, int times) throws TimeoutException { long timeoutLimitInNanos = limitUnit.toNanos(limit); waitUntil(predicate, timeoutLimitInNanos, times); } private void waitUntil(Predicate predicate, long timeoutLimitInNanos, int times) throws TimeoutException { int numberOfMatches = 0; final long startTime = System.nanoTime(); while (System.nanoTime() - startTime < timeoutLimitInNanos) { try { final OutputFrame frame = frames.pollLast(100, TimeUnit.MILLISECONDS); if (frame != null) { LOGGER.debug("{}: {}", frame.getType(), frame.getUtf8StringWithoutLineEnding()); if (predicate.test(frame)) { numberOfMatches++; if (numberOfMatches == times) { return; } } } if (frames.isEmpty()) { // sleep for a moment to avoid excessive CPU spinning Thread.sleep(10L); } } catch (InterruptedException e) { throw new RuntimeException(e); } } // did not return before expiry was reached throw new TimeoutException(); } /** * Wait until Docker closes the stream of output. */ public void waitUntilEnd() { try { waitUntilEnd(Long.MAX_VALUE); } catch (TimeoutException e) { // timeout condition can never occur in a realistic timeframe throw new IllegalStateException(e); } } /** * Wait until Docker closes the stream of output. * * @param limit maximum time to wait * @param limitUnit maximum time to wait (units) */ public void waitUntilEnd(long limit, TimeUnit limitUnit) throws TimeoutException { long expiry = limitUnit.toNanos(limit) + System.nanoTime(); waitUntilEnd(expiry); } private void waitUntilEnd(Long expiry) throws TimeoutException { while (System.nanoTime() < expiry) { try { OutputFrame frame = frames.pollLast(100, TimeUnit.MILLISECONDS); if (frame == OutputFrame.END) { return; } if (frames.isEmpty()) { // sleep for a moment to avoid excessive CPU spinning Thread.sleep(10L); } } catch (InterruptedException e) { throw new RuntimeException(e); } } throw new TimeoutException("Expiry time reached before end of output"); } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/startupcheck/IndefiniteWaitOneShotStartupCheckStrategy.java ================================================ package org.testcontainers.containers.startupcheck; import com.github.dockerjava.api.DockerClient; import com.google.common.util.concurrent.Uninterruptibles; import java.util.concurrent.TimeUnit; /** * Variant of {@link OneShotStartupCheckStrategy} that does not impose a timeout. * Intended for situation such as when a long running task forms part of container startup. *

* It has to be assumed that the container will stop of its own accord, either with a success or failure exit code. */ public class IndefiniteWaitOneShotStartupCheckStrategy extends OneShotStartupCheckStrategy { @Override public boolean waitUntilStartupSuccessful(DockerClient dockerClient, String containerId) { while (checkStartupState(dockerClient, containerId) == StartupStatus.NOT_YET_KNOWN) { Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS); } return checkStartupState(dockerClient, containerId) == StartupStatus.SUCCESSFUL; } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/startupcheck/IsRunningStartupCheckStrategy.java ================================================ package org.testcontainers.containers.startupcheck; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.InspectContainerResponse; import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.DockerStatus; /** * Simplest possible implementation of {@link StartupCheckStrategy} - just check that the container * has reached the running state and has not exited. */ public class IsRunningStartupCheckStrategy extends StartupCheckStrategy { @SuppressWarnings("deprecation") @Override public boolean waitUntilStartupSuccessful(GenericContainer container) { // Optimization: container already has the initial "after start" state, check it first if (checkState(container.getContainerInfo().getState()) == StartupStatus.SUCCESSFUL) { return true; } return super.waitUntilStartupSuccessful(container); } @Override public StartupStatus checkStartupState(DockerClient dockerClient, String containerId) { InspectContainerResponse.ContainerState state = getCurrentState(dockerClient, containerId); return checkState(state); } private StartupStatus checkState(InspectContainerResponse.ContainerState state) { if (Boolean.TRUE.equals(state.getRunning())) { return StartupStatus.SUCCESSFUL; } else if (DockerStatus.isContainerStopped(state)) { if (DockerStatus.isContainerExitCodeSuccess(state)) { return StartupStatus.SUCCESSFUL; } else { return StartupStatus.FAILED; } } else { return StartupStatus.NOT_YET_KNOWN; } } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/startupcheck/MinimumDurationRunningStartupCheckStrategy.java ================================================ package org.testcontainers.containers.startupcheck; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.InspectContainerResponse; import org.jetbrains.annotations.NotNull; import org.testcontainers.utility.DockerStatus; import java.time.Duration; import java.time.Instant; /** * Implementation of {@link StartupCheckStrategy} that checks the container is running and has been running for * a defined minimum period of time. */ public class MinimumDurationRunningStartupCheckStrategy extends StartupCheckStrategy { @NotNull private final Duration minimumRunningDuration; public MinimumDurationRunningStartupCheckStrategy(@NotNull Duration minimumRunningDuration) { this.minimumRunningDuration = minimumRunningDuration; } @Override public StartupStatus checkStartupState(DockerClient dockerClient, String containerId) { // record "now" before fetching status; otherwise the time to fetch the status // will contribute to how long the container has been running. Instant now = Instant.now(); InspectContainerResponse.ContainerState state = getCurrentState(dockerClient, containerId); if (DockerStatus.isContainerRunning(state, minimumRunningDuration, now)) { return StartupStatus.SUCCESSFUL; } else if (DockerStatus.isContainerStopped(state)) { return StartupStatus.FAILED; } return StartupStatus.NOT_YET_KNOWN; } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/startupcheck/OneShotStartupCheckStrategy.java ================================================ package org.testcontainers.containers.startupcheck; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.InspectContainerResponse; import org.testcontainers.utility.DockerStatus; /** * Implementation of {@link StartupCheckStrategy} intended for use with containers that only run briefly and * exit of their own accord. As such, success is deemed to be when the container has stopped with exit code 0. */ public class OneShotStartupCheckStrategy extends StartupCheckStrategy { @Override public StartupStatus checkStartupState(DockerClient dockerClient, String containerId) { InspectContainerResponse.ContainerState state = getCurrentState(dockerClient, containerId); if (!DockerStatus.isContainerStopped(state)) { return StartupStatus.NOT_YET_KNOWN; } if (DockerStatus.isContainerStopped(state) && DockerStatus.isContainerExitCodeSuccess(state)) { return StartupStatus.SUCCESSFUL; } else { return StartupStatus.FAILED; } } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/startupcheck/StartupCheckStrategy.java ================================================ package org.testcontainers.containers.startupcheck; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.InspectContainerResponse; import org.rnorth.ducttape.ratelimits.RateLimiter; import org.rnorth.ducttape.ratelimits.RateLimiterBuilder; import org.rnorth.ducttape.unreliables.Unreliables; import org.testcontainers.containers.GenericContainer; import java.time.Duration; import java.util.concurrent.TimeUnit; /** * Approach to determine whether a container has 'started up' correctly. */ public abstract class StartupCheckStrategy { private static final RateLimiter DOCKER_CLIENT_RATE_LIMITER = RateLimiterBuilder .newBuilder() .withRate(1, TimeUnit.SECONDS) .withConstantThroughput() .build(); private Duration timeout = Duration.ofSeconds(GenericContainer.CONTAINER_RUNNING_TIMEOUT_SEC); @SuppressWarnings("unchecked") public SELF withTimeout(Duration timeout) { this.timeout = timeout; return (SELF) this; } /** * * @deprecated internal API */ @Deprecated public boolean waitUntilStartupSuccessful(GenericContainer container) { return waitUntilStartupSuccessful(container.getDockerClient(), container.getContainerId()); } public boolean waitUntilStartupSuccessful(DockerClient dockerClient, String containerId) { final Boolean[] startedOK = { null }; Unreliables.retryUntilTrue( (int) timeout.toMillis(), TimeUnit.MILLISECONDS, () -> { //noinspection CodeBlock2Expr return DOCKER_CLIENT_RATE_LIMITER.getWhenReady(() -> { StartupStatus state = checkStartupState(dockerClient, containerId); switch (state) { case SUCCESSFUL: startedOK[0] = true; return true; case FAILED: startedOK[0] = false; return true; default: return false; } }); } ); return startedOK[0]; } public abstract StartupStatus checkStartupState(DockerClient dockerClient, String containerId); protected InspectContainerResponse.ContainerState getCurrentState(DockerClient dockerClient, String containerId) { return dockerClient.inspectContainerCmd(containerId).exec().getState(); } public enum StartupStatus { NOT_YET_KNOWN, SUCCESSFUL, FAILED, } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/traits/LinkableContainer.java ================================================ package org.testcontainers.containers.traits; /** * A container which can be linked to by other containers. * * @deprecated Links are deprecated (see #465). Please use {@link org.testcontainers.containers.Network} features instead. */ @Deprecated public interface LinkableContainer { String getContainerName(); } ================================================ FILE: core/src/main/java/org/testcontainers/containers/wait/internal/ExternalPortListeningCheck.java ================================================ package org.testcontainers.containers.wait.internal; import lombok.RequiredArgsConstructor; import org.testcontainers.containers.ContainerState; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; import java.util.Set; import java.util.concurrent.Callable; /** * Mechanism for testing that a socket is listening when run from the test host. */ @RequiredArgsConstructor public class ExternalPortListeningCheck implements Callable { private final ContainerState containerState; private final Set externalLivenessCheckPorts; @Override public Boolean call() { String address = containerState.getHost(); externalLivenessCheckPorts .parallelStream() .forEach(externalPort -> { try (Socket socket = new Socket()) { InetSocketAddress inetSocketAddress = new InetSocketAddress(address, externalPort); socket.connect(inetSocketAddress, 1000); } catch (IOException e) { throw new IllegalStateException("Socket not listening yet: " + externalPort); } }); return true; } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/wait/internal/InternalCommandPortListeningCheck.java ================================================ package org.testcontainers.containers.wait.internal; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.testcontainers.containers.Container.ExecResult; import org.testcontainers.containers.ExecInContainerPattern; import org.testcontainers.containers.wait.strategy.WaitStrategyTarget; import java.time.Duration; import java.time.Instant; import java.util.Set; /** * Mechanism for testing that a socket is listening when run from the container being checked. */ @RequiredArgsConstructor @Slf4j public class InternalCommandPortListeningCheck implements java.util.concurrent.Callable { private final WaitStrategyTarget waitStrategyTarget; private final Set internalPorts; @Override public Boolean call() { StringBuilder command = new StringBuilder("while true; do ( true "); for (int internalPort : internalPorts) { command.append(" && "); command.append(" ("); command.append(String.format("grep -i ':0*%x' /proc/net/tcp*", internalPort)); command.append(" || "); command.append(String.format("nc -vz -w 1 localhost %d", internalPort)); command.append(" || "); command.append(String.format("/bin/bash -c ' getLivenessCheckPorts() { return waitStrategyTarget.getLivenessCheckPortNumbers(); } /** * @return the rate limiter to use */ protected RateLimiter getRateLimiter() { return rateLimiter; } /** * Set the rate limiter being used * * @param rateLimiter rateLimiter * @return this */ public WaitStrategy withRateLimiter(RateLimiter rateLimiter) { this.rateLimiter = rateLimiter; return this; } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategy.java ================================================ package org.testcontainers.containers.wait.strategy; import org.rnorth.ducttape.TimeoutException; import org.rnorth.ducttape.unreliables.Unreliables; import org.testcontainers.containers.ContainerLaunchException; import java.util.concurrent.TimeUnit; /** * Wait strategy leveraging Docker's built-in healthcheck mechanism. * * @see https://docs.docker.com/engine/reference/builder/#healthcheck */ public class DockerHealthcheckWaitStrategy extends AbstractWaitStrategy { @Override protected void waitUntilReady() { try { Unreliables.retryUntilTrue( (int) startupTimeout.getSeconds(), TimeUnit.SECONDS, waitStrategyTarget::isHealthy ); } catch (TimeoutException e) { throw new ContainerLaunchException("Timed out waiting for container to become healthy"); } } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/wait/strategy/HostPortWaitStrategy.java ================================================ package org.testcontainers.containers.wait.strategy; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.awaitility.Awaitility; import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.containers.wait.internal.ExternalPortListeningCheck; import org.testcontainers.containers.wait.internal.InternalCommandPortListeningCheck; import java.time.Duration; import java.time.Instant; import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; /** * Waits until a socket connection can be established on a port exposed or mapped by the container. */ @Slf4j public class HostPortWaitStrategy extends AbstractWaitStrategy { private int[] ports; @Override @SneakyThrows(InterruptedException.class) protected void waitUntilReady() { final Set externalLivenessCheckPorts; if (this.ports == null || this.ports.length == 0) { externalLivenessCheckPorts = getLivenessCheckPorts(); if (externalLivenessCheckPorts.isEmpty()) { if (log.isDebugEnabled()) { log.debug( "Liveness check ports of {} is empty. Not waiting.", waitStrategyTarget.getContainerInfo().getName() ); } return; } } else { externalLivenessCheckPorts = Arrays .stream(this.ports) .mapToObj(port -> waitStrategyTarget.getMappedPort(port)) .collect(Collectors.toSet()); } List exposedPorts = waitStrategyTarget.getExposedPorts(); final Set internalPorts = getInternalPorts(externalLivenessCheckPorts, exposedPorts); Callable internalCheck = new InternalCommandPortListeningCheck(waitStrategyTarget, internalPorts); Callable externalCheck = new ExternalPortListeningCheck( waitStrategyTarget, externalLivenessCheckPorts ); try { List> futures = EXECUTOR.invokeAll( Arrays.asList( // Blocking () -> { Instant now = Instant.now(); Boolean result = internalCheck.call(); log.debug( "Internal port check {} for {} in {}", Boolean.TRUE.equals(result) ? "passed" : "failed", internalPorts, Duration.between(now, Instant.now()) ); return result; }, // Polling () -> { Instant now = Instant.now(); Awaitility .await() .pollInSameThread() .pollInterval(Duration.ofMillis(100)) .pollDelay(Duration.ZERO) .failFast("container is no longer running", () -> !waitStrategyTarget.isRunning()) .ignoreExceptions() .forever() .until(externalCheck); log.debug( "External port check passed for {} mapped as {} in {}", internalPorts, externalLivenessCheckPorts, Duration.between(now, Instant.now()) ); return true; } ), startupTimeout.getSeconds(), TimeUnit.SECONDS ); for (Future future : futures) { future.get(0, TimeUnit.SECONDS); } } catch (CancellationException | ExecutionException | TimeoutException e) { throw new ContainerLaunchException( "Timed out waiting for container port to open (" + waitStrategyTarget.getHost() + " ports: " + externalLivenessCheckPorts + " should be listening)" ); } } private Set getInternalPorts(Set externalLivenessCheckPorts, List exposedPorts) { return exposedPorts .stream() .filter(it -> externalLivenessCheckPorts.contains(waitStrategyTarget.getMappedPort(it))) .collect(Collectors.toSet()); } public HostPortWaitStrategy forPorts(int... ports) { this.ports = ports; return this; } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/wait/strategy/HttpWaitStrategy.java ================================================ package org.testcontainers.containers.wait.strategy; import com.google.common.base.Strings; import com.google.common.io.BaseEncoding; import lombok.extern.slf4j.Slf4j; import org.rnorth.ducttape.TimeoutException; import org.testcontainers.containers.ContainerLaunchException; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.Socket; import java.net.URI; import java.net.URL; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.cert.X509Certificate; import java.time.Duration; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.TrustManager; import javax.net.ssl.X509ExtendedTrustManager; import static org.rnorth.ducttape.unreliables.Unreliables.retryUntilSuccess; @Slf4j public class HttpWaitStrategy extends AbstractWaitStrategy { /** * Authorization HTTP header. */ private static final String HEADER_AUTHORIZATION = "Authorization"; /** * Basic Authorization scheme prefix. */ private static final String AUTH_BASIC = "Basic "; private String path = "/"; private String method = "GET"; private Set statusCodes = new HashSet<>(); private boolean tlsEnabled; private String username; private String password; private final Map headers = new HashMap<>(); private Predicate responsePredicate; private Predicate statusCodePredicate = null; private Optional livenessPort = Optional.empty(); private Duration readTimeout = Duration.ofSeconds(1); private boolean allowInsecure; /** * Waits for the given status code. * * @param statusCode the expected status code * @return this */ public HttpWaitStrategy forStatusCode(int statusCode) { statusCodes.add(statusCode); return this; } /** * Waits for the status code to pass the given predicate * @param statusCodePredicate The predicate to test the response against * @return this */ public HttpWaitStrategy forStatusCodeMatching(Predicate statusCodePredicate) { this.statusCodePredicate = statusCodePredicate; return this; } /** * Waits for the given path. * * @param path the path to check * @return this */ public HttpWaitStrategy forPath(String path) { this.path = path; return this; } /** * Wait for the given port. * * @param port the given port * @return this */ public HttpWaitStrategy forPort(int port) { this.livenessPort = Optional.of(port); return this; } /** * Indicates that the status check should use HTTPS. * * @return this */ public HttpWaitStrategy usingTls() { this.tlsEnabled = true; return this; } /** * Indicates the HTTP method to use (GET by default). * * @param method the HTTP method. * @return this */ public HttpWaitStrategy withMethod(String method) { this.method = method; return this; } /** * Indicates that HTTPS connection could use untrusted (self signed) certificate chains. * * @return this */ public HttpWaitStrategy allowInsecure() { this.allowInsecure = true; return this; } /** * Authenticate with HTTP Basic Authorization credentials. * * @param username the username * @param password the password * @return this */ public HttpWaitStrategy withBasicCredentials(String username, String password) { this.username = username; this.password = password; return this; } /** * Add a custom HTTP Header to the call. * @param name The HTTP Header name * @param value The HTTP Header value * @return this */ public HttpWaitStrategy withHeader(String name, String value) { this.headers.put(name, value); return this; } /** * Add multiple custom HTTP Headers to the call. * @param headers Headers map of name/value * @return this */ public HttpWaitStrategy withHeaders(Map headers) { this.headers.putAll(headers); return this; } /** * Set the HTTP connections read timeout. * * @param timeout the timeout (minimum 1 millisecond) * @return this */ public HttpWaitStrategy withReadTimeout(Duration timeout) { if (timeout.toMillis() < 1) { throw new IllegalArgumentException("you cannot specify a value smaller than 1 ms"); } this.readTimeout = timeout; return this; } /** * Waits for the response to pass the given predicate * @param responsePredicate The predicate to test the response against * @return this */ public HttpWaitStrategy forResponsePredicate(Predicate responsePredicate) { this.responsePredicate = responsePredicate; return this; } @Override protected void waitUntilReady() { final String containerName = waitStrategyTarget.getContainerInfo().getName(); final Integer livenessCheckPort = livenessPort .map(waitStrategyTarget::getMappedPort) .orElseGet(() -> { final Set livenessCheckPorts = getLivenessCheckPorts(); if (livenessCheckPorts == null || livenessCheckPorts.isEmpty()) { log.warn("{}: No exposed ports or mapped ports - cannot wait for status", containerName); return -1; } return livenessCheckPorts.iterator().next(); }); if (null == livenessCheckPort || -1 == livenessCheckPort) { return; } final URI rawUri = buildLivenessUri(livenessCheckPort); final String uri = rawUri.toString(); try { // Un-map the port for logging int originalPort = waitStrategyTarget .getExposedPorts() .stream() .filter(exposedPort -> rawUri.getPort() == waitStrategyTarget.getMappedPort(exposedPort)) .findFirst() .orElseThrow(() -> new IllegalStateException("Target port " + rawUri.getPort() + " is not exposed")); log.info( "{}: Waiting for {} seconds for URL: {} (where port {} maps to container port {})", containerName, startupTimeout.getSeconds(), uri, rawUri.getPort(), originalPort ); } catch (RuntimeException e) { // do not allow a failure in logging to prevent progress, but log for diagnosis log.warn("Unexpected error occurred - will proceed to try to wait anyway", e); } // try to connect to the URL try { retryUntilSuccess( (int) startupTimeout.getSeconds(), TimeUnit.SECONDS, () -> { getRateLimiter() .doWhenReady(() -> { try { final HttpURLConnection connection = openConnection(uri); connection.setReadTimeout(Math.toIntExact(readTimeout.toMillis())); // authenticate if (!Strings.isNullOrEmpty(username)) { connection.setRequestProperty( HEADER_AUTHORIZATION, buildAuthString(username, password) ); connection.setUseCaches(false); } // Add user configured headers this.headers.forEach(connection::setRequestProperty); connection.setRequestMethod(method); connection.connect(); log.trace("Get response code {}", connection.getResponseCode()); // Choose the statusCodePredicate strategy depending on what we defined. Predicate predicate; if (statusCodes.isEmpty() && statusCodePredicate == null) { // We have no status code and no predicate so we expect a 200 OK response code predicate = responseCode -> HttpURLConnection.HTTP_OK == responseCode; } else if (!statusCodes.isEmpty() && statusCodePredicate == null) { // We use the default status predicate checker when we only have status codes predicate = responseCode -> statusCodes.contains(responseCode); } else if (statusCodes.isEmpty()) { // We only have a predicate predicate = statusCodePredicate; } else { // We have both predicate and status code predicate = statusCodePredicate.or(responseCode -> statusCodes.contains(responseCode)); } if (!predicate.test(connection.getResponseCode())) { throw new RuntimeException( String.format("HTTP response code was: %s", connection.getResponseCode()) ); } if (responsePredicate != null) { String responseBody = getResponseBody(connection); log.trace("Get response {}", responseBody); if (!responsePredicate.test(responseBody)) { throw new RuntimeException( String.format("Response: %s did not match predicate", responseBody) ); } } } catch (IOException e) { throw new RuntimeException(e); } }); return true; } ); } catch (TimeoutException e) { throw new ContainerLaunchException( String.format( "Timed out waiting for URL to be accessible (%s should return HTTP %s)", uri, statusCodes.isEmpty() ? HttpURLConnection.HTTP_OK : statusCodes ), e ); } } private HttpURLConnection openConnection(final String uri) throws IOException, MalformedURLException { if (tlsEnabled) { final HttpsURLConnection connection = (HttpsURLConnection) new URL(uri).openConnection(); if (allowInsecure) { // Create a trust manager that does not validate certificate chains // and trust all certificates final TrustManager[] trustAllCerts = new TrustManager[] { new X509ExtendedTrustManager() { @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } @Override public void checkClientTrusted(final X509Certificate[] certs, final String authType) {} @Override public void checkServerTrusted(final X509Certificate[] certs, final String authType) {} @Override public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) {} @Override public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) {} @Override public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) {} @Override public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) {} }, }; try { // Create custom SSL context and set the "trust all certificates" trust manager final SSLContext sc = SSLContext.getInstance("SSL"); sc.init(new KeyManager[0], trustAllCerts, new SecureRandom()); connection.setSSLSocketFactory(sc.getSocketFactory()); } catch (final NoSuchAlgorithmException | KeyManagementException ex) { throw new IOException("Unable to create custom SSL factory instance", ex); } } return connection; } else { return (HttpURLConnection) new URL(uri).openConnection(); } } /** * Build the URI on which to check if the container is ready. * * @param livenessCheckPort the liveness port * @return the liveness URI */ private URI buildLivenessUri(int livenessCheckPort) { final String scheme = (tlsEnabled ? "https" : "http") + "://"; final String host = waitStrategyTarget.getHost(); final String portSuffix; if ((tlsEnabled && 443 == livenessCheckPort) || (!tlsEnabled && 80 == livenessCheckPort)) { portSuffix = ""; } else { portSuffix = ":" + livenessCheckPort; } return URI.create(scheme + host + portSuffix + path); } /** * @param username the username * @param password the password * @return a basic authentication string for the given credentials */ private String buildAuthString(String username, String password) { return AUTH_BASIC + BaseEncoding.base64().encode((username + ":" + password).getBytes()); } private String getResponseBody(HttpURLConnection connection) throws IOException { BufferedReader reader; if (200 <= connection.getResponseCode() && connection.getResponseCode() <= 299) { reader = new BufferedReader(new InputStreamReader((connection.getInputStream()))); } else { reader = new BufferedReader(new InputStreamReader((connection.getErrorStream()))); } StringBuilder builder = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { builder.append(line); } return builder.toString(); } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/wait/strategy/LogMessageWaitStrategy.java ================================================ package org.testcontainers.containers.wait.strategy; import com.github.dockerjava.api.command.LogContainerCmd; import lombok.SneakyThrows; import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.containers.output.FrameConsumerResultCallback; import org.testcontainers.containers.output.OutputFrame; import org.testcontainers.containers.output.WaitingConsumer; import java.io.IOException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Predicate; public class LogMessageWaitStrategy extends AbstractWaitStrategy { private String regEx; private int times = 1; @Override @SneakyThrows(IOException.class) protected void waitUntilReady() { WaitingConsumer waitingConsumer = new WaitingConsumer(); LogContainerCmd cmd = waitStrategyTarget .getDockerClient() .logContainerCmd(waitStrategyTarget.getContainerId()) .withFollowStream(true) .withSince(0) .withStdOut(true) .withStdErr(true); try (FrameConsumerResultCallback callback = new FrameConsumerResultCallback()) { callback.addConsumer(OutputFrame.OutputType.STDOUT, waitingConsumer); callback.addConsumer(OutputFrame.OutputType.STDERR, waitingConsumer); cmd.exec(callback); Predicate waitPredicate = outputFrame -> { // (?s) enables line terminator matching (equivalent to Pattern.DOTALL) return outputFrame.getUtf8String().matches("(?s)" + regEx); }; try { waitingConsumer.waitUntil(waitPredicate, startupTimeout.getSeconds(), TimeUnit.SECONDS, times); } catch (TimeoutException e) { throw new ContainerLaunchException("Timed out waiting for log output matching '" + regEx + "'"); } } } public LogMessageWaitStrategy withRegEx(String regEx) { this.regEx = regEx; return this; } public LogMessageWaitStrategy withTimes(int times) { this.times = times; return this; } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/wait/strategy/ShellStrategy.java ================================================ package org.testcontainers.containers.wait.strategy; import org.rnorth.ducttape.TimeoutException; import org.rnorth.ducttape.unreliables.Unreliables; import org.testcontainers.containers.ContainerLaunchException; import java.util.concurrent.TimeUnit; public class ShellStrategy extends AbstractWaitStrategy { private String command; public ShellStrategy withCommand(String command) { this.command = command; return this; } @Override protected void waitUntilReady() { try { Unreliables.retryUntilTrue( (int) startupTimeout.getSeconds(), TimeUnit.SECONDS, () -> waitStrategyTarget.execInContainer("/bin/sh", "-c", this.command).getExitCode() == 0 ); } catch (TimeoutException e) { throw new ContainerLaunchException( "Timed out waiting for container to execute `" + this.command + "` successfully." ); } } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/wait/strategy/Wait.java ================================================ package org.testcontainers.containers.wait.strategy; /** * Convenience class with logic for building common {@link WaitStrategy} instances. * */ public class Wait { /** * Convenience method to return the default WaitStrategy. * * @return a WaitStrategy */ public static WaitStrategy defaultWaitStrategy() { return forListeningPort(); } /** * Convenience method to return a WaitStrategy for an exposed or mapped port. * * @return the WaitStrategy * @see HostPortWaitStrategy */ public static HostPortWaitStrategy forListeningPort() { return new HostPortWaitStrategy(); } /** * Convenience method to return a WaitStrategy for exposed or mapped ports. * * @param ports the port to check * @return the WaitStrategy */ public static HostPortWaitStrategy forListeningPorts(int... ports) { return new HostPortWaitStrategy().forPorts(ports); } /** * Convenience method to return a WaitStrategy for an HTTP endpoint. * * @param path the path to check * @return the WaitStrategy * @see HttpWaitStrategy */ public static HttpWaitStrategy forHttp(String path) { return new HttpWaitStrategy().forPath(path); } /** * Convenience method to return a WaitStrategy for an HTTPS endpoint. * * @param path the path to check * @return the WaitStrategy * @see HttpWaitStrategy */ public static HttpWaitStrategy forHttps(String path) { return forHttp(path).usingTls(); } /** * Convenience method to return a WaitStrategy for log messages. * * @param regex the regex pattern to check for * @param times the number of times the pattern is expected * @return LogMessageWaitStrategy */ public static LogMessageWaitStrategy forLogMessage(String regex, int times) { return new LogMessageWaitStrategy().withRegEx(regex).withTimes(times); } /** * Convenience method to return a WaitStrategy leveraging Docker's built-in healthcheck. * * @return DockerHealthcheckWaitStrategy */ public static DockerHealthcheckWaitStrategy forHealthcheck() { return new DockerHealthcheckWaitStrategy(); } /** * Convenience method to return a WaitStrategy for a shell command. * * @param command the command to run * @return ShellStrategy */ public static ShellStrategy forSuccessfulCommand(String command) { return new ShellStrategy().withCommand(command); } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/wait/strategy/WaitAllStrategy.java ================================================ package org.testcontainers.containers.wait.strategy; import org.rnorth.ducttape.timeouts.Timeouts; import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; public class WaitAllStrategy implements WaitStrategy { public enum Mode { /** * This is the default mode: The timeout of the {@link WaitAllStrategy strategy} is applied to each individual * strategy, so that the container waits maximum for * {@link org.testcontainers.containers.wait.strategy.WaitAllStrategy#timeout}. */ WITH_OUTER_TIMEOUT, /** * Using this mode triggers the following behaviour: The outer timeout is disabled and the outer enclosing * strategy waits for all inner strategies according to their timeout. Once set, it disables * {@link org.testcontainers.containers.wait.strategy.WaitAllStrategy#withStartupTimeout(java.time.Duration)} method, * as it would overwrite inner timeouts. */ WITH_INDIVIDUAL_TIMEOUTS_ONLY, /** * This is the original mode of this strategy: The inner strategies wait with their preconfigured timeout * individually and the wait all strategy kills them, if the outer limit is reached. */ WITH_MAXIMUM_OUTER_TIMEOUT, } private final Mode mode; private final List strategies = new ArrayList<>(); private Duration timeout = Duration.ofSeconds(30); public WaitAllStrategy() { this(Mode.WITH_OUTER_TIMEOUT); } public WaitAllStrategy(Mode mode) { this.mode = mode; } @Override public void waitUntilReady(WaitStrategyTarget waitStrategyTarget) { if (mode == Mode.WITH_INDIVIDUAL_TIMEOUTS_ONLY) { waitUntilNestedStrategiesAreReady(waitStrategyTarget); } else { Timeouts.doWithTimeout( (int) timeout.toMillis(), TimeUnit.MILLISECONDS, () -> { waitUntilNestedStrategiesAreReady(waitStrategyTarget); } ); } } private void waitUntilNestedStrategiesAreReady(WaitStrategyTarget waitStrategyTarget) { for (WaitStrategy strategy : strategies) { strategy.waitUntilReady(waitStrategyTarget); } } public WaitAllStrategy withStrategy(WaitStrategy strategy) { if (mode == Mode.WITH_OUTER_TIMEOUT) { applyStartupTimeout(strategy); } this.strategies.add(strategy); return this; } @Override public WaitAllStrategy withStartupTimeout(Duration startupTimeout) { if (mode == Mode.WITH_INDIVIDUAL_TIMEOUTS_ONLY) { throw new IllegalStateException( String.format( "Changing startup timeout is not supported with mode %s", Mode.WITH_INDIVIDUAL_TIMEOUTS_ONLY ) ); } this.timeout = startupTimeout; strategies.forEach(this::applyStartupTimeout); return this; } private void applyStartupTimeout(WaitStrategy childStrategy) { childStrategy.withStartupTimeout(this.timeout); } } ================================================ FILE: core/src/main/java/org/testcontainers/containers/wait/strategy/WaitStrategy.java ================================================ package org.testcontainers.containers.wait.strategy; import java.time.Duration; public interface WaitStrategy { void waitUntilReady(WaitStrategyTarget waitStrategyTarget); WaitStrategy withStartupTimeout(Duration startupTimeout); } ================================================ FILE: core/src/main/java/org/testcontainers/containers/wait/strategy/WaitStrategyTarget.java ================================================ package org.testcontainers.containers.wait.strategy; import org.testcontainers.containers.ContainerState; import java.util.Set; import java.util.stream.Collectors; public interface WaitStrategyTarget extends ContainerState { /** * @return the ports on which to check if the container is ready */ default Set getLivenessCheckPortNumbers() { final Set result = getExposedPorts() .stream() .map(this::getMappedPort) .distinct() .collect(Collectors.toSet()); result.addAll(getBoundPortNumbers()); return result; } } ================================================ FILE: core/src/main/java/org/testcontainers/core/CreateContainerCmdModifier.java ================================================ package org.testcontainers.core; import com.github.dockerjava.api.command.CreateContainerCmd; /** * Callback interface that can be used to customize a {@link CreateContainerCmd}. */ public interface CreateContainerCmdModifier { /** * Callback to modify a {@link CreateContainerCmd} instance. */ CreateContainerCmd modify(CreateContainerCmd createContainerCmd); } ================================================ FILE: core/src/main/java/org/testcontainers/dockerclient/AuditLoggingDockerClient.java ================================================ package org.testcontainers.dockerclient; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.CreateContainerCmd; import com.github.dockerjava.api.command.CreateNetworkCmd; import com.github.dockerjava.api.command.KillContainerCmd; import com.github.dockerjava.api.command.RemoveContainerCmd; import com.github.dockerjava.api.command.RemoveNetworkCmd; import com.github.dockerjava.api.command.StartContainerCmd; import com.github.dockerjava.api.command.StopContainerCmd; import com.github.dockerjava.api.command.SyncDockerCmd; import lombok.experimental.Delegate; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.testcontainers.utility.AuditLogger; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; import java.util.function.BiConsumer; /** * Wrapper for {@link DockerClient} to facilitate 'audit logging' of potentially destruction actions using * {@link org.testcontainers.utility.AuditLogger}. * */ @Slf4j @SuppressWarnings("unchecked") class AuditLoggingDockerClient implements DockerClient { @Delegate(excludes = InterceptedMethods.class) private final DockerClient wrappedClient; public AuditLoggingDockerClient(DockerClient wrappedClient) { this.wrappedClient = wrappedClient; } @Override public CreateContainerCmd createContainerCmd(@NotNull String image) { return wrappedCommand( CreateContainerCmd.class, wrappedClient.createContainerCmd(image), (cmd, res) -> AuditLogger.doLog("CREATE", image, res.getId(), cmd), (cmd, e) -> AuditLogger.doLog("CREATE", image, null, cmd, e) ); } @Override public StartContainerCmd startContainerCmd(@NotNull String containerId) { return wrappedCommand( StartContainerCmd.class, wrappedClient.startContainerCmd(containerId), (cmd, res) -> AuditLogger.doLog("START", null, containerId, cmd), (cmd, e) -> AuditLogger.doLog("START", null, containerId, cmd, e) ); } @Override public RemoveContainerCmd removeContainerCmd(@NotNull String containerId) { return wrappedCommand( RemoveContainerCmd.class, wrappedClient.removeContainerCmd(containerId), (cmd, res) -> AuditLogger.doLog("REMOVE", null, containerId, cmd), (cmd, e) -> AuditLogger.doLog("REMOVE", null, containerId, cmd, e) ); } @Override public StopContainerCmd stopContainerCmd(@NotNull String containerId) { return wrappedCommand( StopContainerCmd.class, wrappedClient.stopContainerCmd(containerId), (cmd, res) -> AuditLogger.doLog("STOP", null, containerId, cmd), (cmd, e) -> AuditLogger.doLog("STOP", null, containerId, cmd, e) ); } @Override public KillContainerCmd killContainerCmd(@NotNull String containerId) { return wrappedCommand( KillContainerCmd.class, wrappedClient.killContainerCmd(containerId), (cmd, res) -> AuditLogger.doLog("KILL", null, containerId, cmd), (cmd, e) -> AuditLogger.doLog("KILL", null, containerId, cmd, e) ); } @Override public CreateNetworkCmd createNetworkCmd() { return wrappedCommand( CreateNetworkCmd.class, wrappedClient.createNetworkCmd(), (cmd, res) -> AuditLogger.doLog("CREATE_NETWORK", null, null, cmd), (cmd, e) -> AuditLogger.doLog("CREATE_NETWORK", null, null, cmd, e) ); } @Override public RemoveNetworkCmd removeNetworkCmd(@NotNull String networkId) { return wrappedCommand( RemoveNetworkCmd.class, wrappedClient.removeNetworkCmd(networkId), (cmd, res) -> AuditLogger.doLog("REMOVE_NETWORK", null, null, cmd), (cmd, e) -> AuditLogger.doLog("REMOVE_NETWORK", null, null, cmd, e) ); } private , R> T wrappedCommand( Class clazz, T cmd, BiConsumer successConsumer, BiConsumer failureConsumer ) { return (T) Proxy.newProxyInstance( clazz.getClassLoader(), new Class[] { clazz }, (proxy, method, args) -> { if (method.getName().equals("exec")) { try { R r = (R) method.invoke(cmd, args); successConsumer.accept(cmd, r); return r; } catch (Exception e) { if (e instanceof InvocationTargetException && e.getCause() instanceof Exception) { e = (Exception) e.getCause(); } failureConsumer.accept(cmd, e); throw e; } } else { return method.invoke(cmd, args); } } ); } @SuppressWarnings("unused") private interface InterceptedMethods { CreateContainerCmd createContainerCmd(String image); StartContainerCmd startContainerCmd(String containerId); RemoveContainerCmd removeContainerCmd(String containerId); StopContainerCmd stopContainerCmd(String containerId); KillContainerCmd killContainerCmd(String containerId); CreateNetworkCmd createNetworkCmd(); RemoveNetworkCmd removeNetworkCmd(String networkId); } } ================================================ FILE: core/src/main/java/org/testcontainers/dockerclient/AuthDelegatingDockerClientConfig.java ================================================ package org.testcontainers.dockerclient; import com.github.dockerjava.api.model.AuthConfig; import com.github.dockerjava.core.DockerClientConfig; import com.github.dockerjava.core.DockerClientConfigDelegate; import lombok.extern.slf4j.Slf4j; import org.testcontainers.utility.AuthConfigUtil; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.RegistryAuthLocator; /** * Facade implementation for {@link DockerClientConfig} which overrides how authentication * configuration is obtained. A delegate {@link DockerClientConfig} will be called first * to try and obtain auth credentials, but after that {@link RegistryAuthLocator} will be * used to try and improve the auth resolution (e.g. using credential helpers). * * TODO move to docker-java */ @Slf4j class AuthDelegatingDockerClientConfig extends DockerClientConfigDelegate { public AuthDelegatingDockerClientConfig(DockerClientConfig delegate) { super(delegate); } @Override public AuthConfig effectiveAuthConfig(String imageName) { // allow docker-java auth config to be used as a fallback AuthConfig fallbackAuthConfig; try { fallbackAuthConfig = super.effectiveAuthConfig(imageName); } catch (Exception e) { log.debug( "Delegate call to effectiveAuthConfig failed with cause: '{}'. " + "Resolution of auth config will continue using RegistryAuthLocator.", e.getMessage() ); fallbackAuthConfig = new AuthConfig(); } // try and obtain more accurate auth config using our resolution final DockerImageName parsed = DockerImageName.parse(imageName); final AuthConfig effectiveAuthConfig = RegistryAuthLocator .instance() .lookupAuthConfig(parsed, fallbackAuthConfig); log.debug("Effective auth config [{}]", AuthConfigUtil.toSafeString(effectiveAuthConfig)); return effectiveAuthConfig; } } ================================================ FILE: core/src/main/java/org/testcontainers/dockerclient/DockerClientConfigUtils.java ================================================ package org.testcontainers.dockerclient; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.testcontainers.DockerClientFactory; import java.io.File; import java.net.URI; import java.util.Optional; import java.util.concurrent.TimeUnit; @Slf4j public class DockerClientConfigUtils { // See https://github.com/docker/docker/blob/a9fa38b1edf30b23cae3eade0be48b3d4b1de14b/daemon/initlayer/setup_unix.go#L25 public static final boolean IN_A_CONTAINER = new File("/.dockerenv").exists(); @Getter(lazy = true) private static final Optional defaultGateway = Optional .ofNullable( DockerClientFactory .instance() .runInsideDocker( cmd -> cmd.withCmd("sh", "-c", "ip route|awk '/default/ { print $3 }'"), (client, id) -> { try { LogToStringContainerCallback loggingCallback = new LogToStringContainerCallback(); client .logContainerCmd(id) .withStdOut(true) .withFollowStream(true) .exec(loggingCallback) .awaitStarted(); loggingCallback.awaitCompletion(3, TimeUnit.SECONDS); return loggingCallback.toString(); } catch (Exception e) { log.warn("Can't parse the default gateway IP", e); return null; } } ) ) .map(StringUtils::trimToEmpty) .filter(StringUtils::isNotBlank); /** * @deprecated use {@link DockerClientProviderStrategy#getDockerHostIpAddress()} */ @Deprecated public static String getDockerHostIpAddress(URI dockerHost) { switch (dockerHost.getScheme()) { case "http": case "https": case "tcp": return dockerHost.getHost(); case "unix": case "npipe": if (IN_A_CONTAINER) { return getDefaultGateway().orElse("localhost"); } return "localhost"; default: return null; } } } ================================================ FILE: core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java ================================================ package org.testcontainers.dockerclient; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.model.Info; import com.github.dockerjava.api.model.Network; import com.github.dockerjava.core.DefaultDockerClientConfig; import com.github.dockerjava.core.DockerClientImpl; import com.github.dockerjava.core.RemoteApiVersion; import com.github.dockerjava.transport.DockerHttpClient; import com.github.dockerjava.transport.NamedPipeSocket; import com.github.dockerjava.transport.SSLConfig; import com.github.dockerjava.transport.UnixSocket; import com.github.dockerjava.zerodep.ZerodepDockerHttpClient; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Throwables; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.awaitility.Awaitility; import org.jetbrains.annotations.Nullable; import org.rnorth.ducttape.TimeoutException; import org.rnorth.ducttape.ratelimits.RateLimiter; import org.rnorth.ducttape.ratelimits.RateLimiterBuilder; import org.rnorth.ducttape.unreliables.Unreliables; import org.testcontainers.DockerClientFactory; import org.testcontainers.UnstableAPI; import org.testcontainers.utility.TestcontainersConfiguration; import java.io.File; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; import java.net.URI; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.time.Duration; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.net.SocketFactory; /** * Mechanism to find a viable Docker client configuration according to the host system environment. *

* The order is: *

    *
  • {@code TestcontainersHostPropertyClientProviderStrategy}
  • *
  • {@code EnvironmentAndSystemPropertyClientProviderStrategy}
  • *
  • Persistable {@code DockerClientProviderStrategy} in ~/.testcontainers.properties
  • *
  • Other strategies order by priority
  • *
*/ @Slf4j public abstract class DockerClientProviderStrategy { @Getter(lazy = true) private final DockerClient dockerClient = getClientForConfig(getTransportConfig()); private String dockerHostIpAddress; @Getter private Info info; private final RateLimiter PING_RATE_LIMITER = RateLimiterBuilder .newBuilder() .withRate(10, TimeUnit.SECONDS) .withConstantThroughput() .build(); private static final AtomicBoolean FAIL_FAST_ALWAYS = new AtomicBoolean(false); /** * @return a short textual description of the strategy */ public abstract String getDescription(); protected boolean isApplicable() { return true; } protected boolean isPersistable() { return true; } public boolean allowUserOverrides() { return true; } /** /* @return the path under which the Docker unix socket is reachable relative to the Docker daemon */ public String getRemoteDockerUnixSocketPath() { return null; } /** * @return highest to lowest priority value */ protected int getPriority() { return 0; } /** * @throws InvalidConfigurationException if this strategy fails */ public abstract TransportConfig getTransportConfig() throws InvalidConfigurationException; /** * @return a usable, tested, Docker client configuration for the host system environment * * @deprecated use {@link #getDockerClient()} */ @Deprecated public DockerClient getClient() { DockerClient dockerClient = getDockerClient(); try { Unreliables.retryUntilSuccess( 30, TimeUnit.SECONDS, () -> { return PING_RATE_LIMITER.getWhenReady(() -> { log.debug("Pinging docker daemon..."); dockerClient.pingCmd().exec(); log.debug("Pinged"); return true; }); } ); } catch (TimeoutException e) { IOUtils.closeQuietly(dockerClient); throw e; } return dockerClient; } /** * TODO we should consider moving this to docker-java at some point */ @UnstableAPI protected boolean test() { TransportConfig transportConfig = getTransportConfig(); URI dockerHost = transportConfig.getDockerHost(); Callable socketProvider; SocketAddress socketAddress; switch (dockerHost.getScheme()) { case "tcp": case "http": case "https": SocketFactory socketFactory = SocketFactory.getDefault(); SSLConfig sslConfig = transportConfig.getSslConfig(); if (sslConfig != null) { try { socketFactory = sslConfig.getSSLContext().getSocketFactory(); } catch ( KeyManagementException | UnrecoverableKeyException | NoSuchAlgorithmException | KeyStoreException e ) { log.warn("Exception while creating SSLSocketFactory", e); return false; } } socketProvider = socketFactory::createSocket; socketAddress = new InetSocketAddress(dockerHost.getHost(), dockerHost.getPort()); break; case "unix": case "npipe": if (!new File(dockerHost.getPath()).exists()) { log.debug("DOCKER_HOST socket file '{}' does not exist", dockerHost.getPath()); return false; } socketProvider = () -> { switch (dockerHost.getScheme()) { case "unix": return UnixSocket.get(dockerHost.getPath()); case "npipe": return new NamedPipeSocket(dockerHost.getPath()); default: throw new IllegalStateException("Unexpected scheme " + dockerHost.getScheme()); } }; socketAddress = new InetSocketAddress("localhost", 2375); break; default: log.warn("Unknown DOCKER_HOST scheme {}, skipping the strategy test...", dockerHost.getScheme()); return true; } try (Socket socket = socketProvider.call()) { Awaitility .await() .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 .untilAsserted(() -> socket.connect(socketAddress)); return true; } catch (Exception e) { log.warn("DOCKER_HOST {} is not listening", dockerHost, e); return false; } } /** * Determine the right DockerClientConfig to use for building clients by trial-and-error. * * @return a working DockerClientConfig, as determined by successful execution of a ping command */ public static DockerClientProviderStrategy getFirstValidStrategy(List strategies) { if (FAIL_FAST_ALWAYS.get()) { throw new IllegalStateException( "Previous attempts to find a Docker environment failed. Will not retry. Please see logs and check configuration" ); } List configurationFailures = new ArrayList<>(); List allStrategies = new ArrayList<>(); // Manually enforce priority independent of priority property of strategy allStrategies.add(new TestcontainersHostPropertyClientProviderStrategy()); allStrategies.add(new EnvironmentAndSystemPropertyClientProviderStrategy()); // Next strategy to try out is the one configured using the Testcontainers configuration mechanism loadConfiguredStrategy().ifPresent(allStrategies::add); // Finally, add all other strategies ordered by their internal priority strategies .stream() .sorted(Comparator.comparing(DockerClientProviderStrategy::getPriority).reversed()) .collect(Collectors.toCollection(() -> allStrategies)); Predicate distinctStrategyClassPredicate = new Predicate() { final Set> classes = new HashSet<>(); @Override public boolean test(DockerClientProviderStrategy dockerClientProviderStrategy) { return classes.add(dockerClientProviderStrategy.getClass()); } }; return allStrategies .stream() .filter(distinctStrategyClassPredicate) .filter(DockerClientProviderStrategy::isApplicable) .filter(strategy -> tryOutStrategy(configurationFailures, strategy)) .findFirst() .orElseThrow(() -> { log.error( "Could not find a valid Docker environment. Please check configuration. Attempted configurations were:\n" + configurationFailures.stream().map(it -> "\t" + it).collect(Collectors.joining("\n")) + "As no valid configuration was found, execution cannot continue.\n" + "See https://java.testcontainers.org/on_failure.html for more details." ); FAIL_FAST_ALWAYS.set(true); return new IllegalStateException( "Could not find a valid Docker environment. Please see logs and check configuration" ); }); } private static boolean tryOutStrategy(List configurationFailures, DockerClientProviderStrategy strategy) { try { log.debug("Trying out strategy: {}", strategy.getClass().getSimpleName()); if (!strategy.test()) { log.debug("strategy {} did not pass the test", strategy.getClass().getSimpleName()); return false; } strategy.info = strategy.getDockerClient().infoCmd().exec(); log.info("Found Docker environment with {}", strategy.getDescription()); log.debug( "Transport type: '{}', Docker host: '{}'", TestcontainersConfiguration.getInstance().getTransportType(), strategy.getTransportConfig().getDockerHost() ); log.debug("Checking Docker OS type for {}", strategy.getDescription()); String osType = strategy.getInfo().getOsType(); if (StringUtils.isBlank(osType)) { log.warn("Could not determine Docker OS type"); } else if (!osType.equals("linux")) { log.warn("{} is currently not supported", osType); throw new InvalidConfigurationException(osType + " containers are currently not supported"); } if (strategy.isPersistable()) { TestcontainersConfiguration .getInstance() .updateUserConfig("docker.client.strategy", strategy.getClass().getName()); } return true; } catch (Exception | ExceptionInInitializerError | NoClassDefFoundError e) { @Nullable String throwableMessage = e.getMessage(); @SuppressWarnings("ThrowableResultOfMethodCallIgnored") Throwable rootCause = Throwables.getRootCause(e); @Nullable String rootCauseMessage = rootCause.getMessage(); String failureDescription; if (throwableMessage != null && throwableMessage.equals(rootCauseMessage)) { failureDescription = String.format( "%s: failed with exception %s (%s)", strategy.getClass().getSimpleName(), e.getClass().getSimpleName(), throwableMessage ); } else { failureDescription = String.format( "%s: failed with exception %s (%s). Root cause %s (%s)", strategy.getClass().getSimpleName(), e.getClass().getSimpleName(), throwableMessage, rootCause.getClass().getSimpleName(), rootCauseMessage ); } configurationFailures.add(failureDescription); log.debug(failureDescription); return false; } } private static Optional loadConfiguredStrategy() { String configuredDockerClientStrategyClassName = TestcontainersConfiguration .getInstance() .getDockerClientStrategyClassName(); return Stream .of(configuredDockerClientStrategyClassName) .filter(Objects::nonNull) .flatMap(it -> { try { Class strategyClass = (Class) Thread .currentThread() .getContextClassLoader() .loadClass(it); return Stream.of(strategyClass.newInstance()); } catch (ClassNotFoundException e) { log.warn( "Can't instantiate a strategy from {} (ClassNotFoundException). " + "This probably means that cached configuration refers to a client provider " + "class that is not available in this version of Testcontainers. Other " + "strategies will be tried instead.", it ); return Stream.empty(); } catch (InstantiationException | IllegalAccessException e) { log.warn("Can't instantiate a strategy from {}", it, e); return Stream.empty(); } }) // Ignore persisted strategy if it's not persistable anymore .filter(DockerClientProviderStrategy::isPersistable) .peek(strategy -> { log.info( "Loaded {} from ~/.testcontainers.properties, will try it first", strategy.getClass().getName() ); }) .findFirst(); } public static DockerClient getClientForConfig(TransportConfig transportConfig) { final DockerHttpClient dockerHttpClient; String transportType = TestcontainersConfiguration.getInstance().getTransportType(); switch (transportType) { case "httpclient5": dockerHttpClient = new ZerodepDockerHttpClient.Builder() .dockerHost(transportConfig.getDockerHost()) .sslConfig(transportConfig.getSslConfig()) .build(); break; default: throw new IllegalArgumentException("Unknown transport type '" + transportType + "'"); } DefaultDockerClientConfig.Builder configBuilder = DefaultDockerClientConfig .createDefaultConfigBuilder() .withDockerHost(transportConfig.getDockerHost().toString()); Map headers = new HashMap<>(); headers.put("x-tc-sid", DockerClientFactory.SESSION_ID); headers.put("User-Agent", String.format("tc-java/%s", DockerClientFactory.TESTCONTAINERS_VERSION)); try { if (configBuilder.build().getApiVersion() == RemoteApiVersion.UNKNOWN_VERSION) { configBuilder.withApiVersion(RemoteApiVersion.VERSION_1_44); } DockerClient client = DockerClientImpl.getInstance( new AuthDelegatingDockerClientConfig(configBuilder.build()), new HeadersAddingDockerHttpClient(dockerHttpClient, headers) ); log.debug("Pinging Docker API version 1.44."); client.pingCmd().exec(); return client; } catch (Exception ex) { log.debug("Fallback to Docker API version 1.32."); return DockerClientImpl.getInstance( new AuthDelegatingDockerClientConfig( configBuilder.withApiVersion(RemoteApiVersion.VERSION_1_32).build() ), new HeadersAddingDockerHttpClient(dockerHttpClient, headers) ); } } public synchronized String getDockerHostIpAddress() { if (dockerHostIpAddress == null) { dockerHostIpAddress = resolveDockerHostIpAddress( getDockerClient(), getTransportConfig().getDockerHost(), allowUserOverrides() ); } return dockerHostIpAddress; } @VisibleForTesting static String resolveDockerHostIpAddress(DockerClient client, URI dockerHost, boolean allowUserOverrides) { if (allowUserOverrides) { String hostOverride = System.getenv("TESTCONTAINERS_HOST_OVERRIDE"); if (!StringUtils.isBlank(hostOverride)) { return hostOverride; } } switch (dockerHost.getScheme()) { case "http": case "https": case "tcp": return dockerHost.getHost(); case "unix": case "npipe": if (DockerClientConfigUtils.IN_A_CONTAINER) { return client .inspectNetworkCmd() .withNetworkId("bridge") .exec() .getIpam() .getConfig() .stream() .filter(it -> it.getGateway() != null) .findAny() .map(Network.Ipam.Config::getGateway) .orElseGet(() -> { return DockerClientConfigUtils.getDefaultGateway().orElse("localhost"); }); } return "localhost"; default: return null; } } } ================================================ FILE: core/src/main/java/org/testcontainers/dockerclient/DockerDesktopClientProviderStrategy.java ================================================ package org.testcontainers.dockerclient; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.SystemUtils; import org.jetbrains.annotations.Nullable; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; /** * Look at the following paths: *
    *
  • Linux: ~/.docker/desktop/docker.sock
  • *
  • MacOS: ~/.docker/run/docker.sock
  • *
* * @deprecated this class is used by the SPI and should not be used directly */ @Slf4j @Deprecated public class DockerDesktopClientProviderStrategy extends DockerClientProviderStrategy { public static final int PRIORITY = UnixSocketClientProviderStrategy.PRIORITY - 1; @Getter(lazy = true) @Nullable private final Path socketPath = resolveSocketPath(); private Path resolveSocketPath() { Path linuxPath = Paths.get(System.getProperty("user.home")).resolve(".docker").resolve("desktop"); return tryFolder(linuxPath) .orElseGet(() -> { Path macosPath = Paths.get(System.getProperty("user.home")).resolve(".docker").resolve("run"); return tryFolder(macosPath).orElse(null); }); } @Override public String getDescription() { return "Docker accessed via Unix socket (" + getSocketPath() + ")"; } @Override public TransportConfig getTransportConfig() throws InvalidConfigurationException { return TransportConfig.builder().dockerHost(URI.create("unix://" + getSocketPath().toString())).build(); } @Override protected int getPriority() { return PRIORITY; } @Override protected boolean isPersistable() { return false; } @Override public String getRemoteDockerUnixSocketPath() { return "/var/run/docker.sock"; } @Override protected boolean isApplicable() { return (SystemUtils.IS_OS_LINUX || SystemUtils.IS_OS_MAC) && this.socketPath != null; } private Optional tryFolder(Path path) { if (!Files.exists(path)) { log.debug("'{}' does not exist.", path); return Optional.empty(); } Path socketPath = path.resolve("docker.sock"); if (!Files.exists(socketPath)) { log.debug("'{}' does not exist.", socketPath); return Optional.empty(); } return Optional.of(socketPath); } } ================================================ FILE: core/src/main/java/org/testcontainers/dockerclient/DockerMachineClientProviderStrategy.java ================================================ package org.testcontainers.dockerclient; import com.github.dockerjava.core.LocalDirectorySSLConfig; import com.google.common.base.Preconditions; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.testcontainers.utility.CommandLine; import org.testcontainers.utility.DockerMachineClient; import java.net.URI; import java.nio.file.Paths; import java.util.Arrays; import java.util.Optional; /** * Use Docker machine (if available on the PATH) to locate a Docker environment. * * @deprecated this class is used by the SPI and should not be used directly */ @Slf4j @Deprecated public final class DockerMachineClientProviderStrategy extends DockerClientProviderStrategy { @Getter(lazy = true) private final TransportConfig transportConfig = resolveTransportConfig(); private TransportConfig resolveTransportConfig() throws InvalidConfigurationException { boolean installed = DockerMachineClient.instance().isInstalled(); Preconditions.checkArgument( installed, "docker-machine executable was not found on PATH (" + Arrays.toString(CommandLine.getSystemPath()) + ")" ); Optional machineNameOptional = DockerMachineClient.instance().getDefaultMachine(); Preconditions.checkArgument( machineNameOptional.isPresent(), "docker-machine is installed but no default machine could be found" ); String machineName = machineNameOptional.get(); log.info("Found docker-machine, and will use machine named {}", machineName); DockerMachineClient.instance().ensureMachineRunning(machineName); String dockerDaemonUrl = DockerMachineClient.instance().getDockerDaemonUrl(machineName); log.info("Docker daemon URL for docker machine {} is {}", machineName, dockerDaemonUrl); return TransportConfig .builder() .dockerHost(URI.create(dockerDaemonUrl)) .sslConfig( new LocalDirectorySSLConfig( Paths.get(System.getProperty("user.home") + "/.docker/machine/certs/").toString() ) ) .build(); } @Override protected boolean isApplicable() { boolean installed = DockerMachineClient.instance().isInstalled(); if (!installed) { log.info( "docker-machine executable was not found on PATH ({})", Arrays.toString(CommandLine.getSystemPath()) ); return false; } Optional machineNameOptional = DockerMachineClient.instance().getDefaultMachine(); if (!machineNameOptional.isPresent()) { log.info("docker-machine is installed but no default machine could be found"); } return true; } @Override protected boolean isPersistable() { return false; } @Override protected int getPriority() { return EnvironmentAndSystemPropertyClientProviderStrategy.PRIORITY - 100; } @Override public String getDescription() { return "docker-machine"; } } ================================================ FILE: core/src/main/java/org/testcontainers/dockerclient/EnvironmentAndSystemPropertyClientProviderStrategy.java ================================================ package org.testcontainers.dockerclient; import com.github.dockerjava.core.DefaultDockerClientConfig; import com.github.dockerjava.core.DockerClientConfig; import lombok.Getter; import org.testcontainers.utility.TestcontainersConfiguration; import java.util.Optional; /** * Use environment variables and system properties (as supported by the underlying DockerClient DefaultConfigBuilder) * to try and locate a docker environment. *

* Resolution order is: *

    *
  1. DOCKER_HOST env var
  2. *
  3. docker.host in ~/.testcontainers.properties
  4. *
* * @deprecated this class is used by the SPI and should not be used directly */ @Deprecated public final class EnvironmentAndSystemPropertyClientProviderStrategy extends DockerClientProviderStrategy { public static final int PRIORITY = 100; private final DockerClientConfig dockerClientConfig; @Getter private final boolean applicable; public EnvironmentAndSystemPropertyClientProviderStrategy() { // use docker-java defaults if present, overridden if our own configuration is set this(DefaultDockerClientConfig.createDefaultConfigBuilder()); } EnvironmentAndSystemPropertyClientProviderStrategy(DefaultDockerClientConfig.Builder configBuilder) { String dockerConfigSource = TestcontainersConfiguration .getInstance() .getEnvVarOrProperty("dockerconfig.source", "auto"); switch (dockerConfigSource) { case "auto": Optional dockerHost = getSetting("docker.host"); dockerHost.ifPresent(configBuilder::withDockerHost); applicable = dockerHost.isPresent(); getSetting("docker.tls.verify").ifPresent(configBuilder::withDockerTlsVerify); getSetting("docker.cert.path").ifPresent(configBuilder::withDockerCertPath); break; case "autoIgnoringUserProperties": applicable = configBuilder.isDockerHostSetExplicitly(); break; default: throw new InvalidConfigurationException("Invalid value for dockerconfig.source: " + dockerConfigSource); } dockerClientConfig = configBuilder.build(); } private Optional getSetting(final String name) { return Optional.ofNullable(TestcontainersConfiguration.getInstance().getEnvVarOrUserProperty(name, null)); } @Override public TransportConfig getTransportConfig() { return TransportConfig .builder() .dockerHost(dockerClientConfig.getDockerHost()) .sslConfig(dockerClientConfig.getSSLConfig()) .build(); } @Override protected int getPriority() { return PRIORITY; } @Override public String getDescription() { return ( "Environment variables, system properties and defaults. Resolved dockerHost=" + dockerClientConfig.getDockerHost() ); } @Override protected boolean isPersistable() { return false; } } ================================================ FILE: core/src/main/java/org/testcontainers/dockerclient/HeadersAddingDockerHttpClient.java ================================================ package org.testcontainers.dockerclient; import com.github.dockerjava.transport.DockerHttpClient; import lombok.RequiredArgsConstructor; import lombok.ToString; import lombok.experimental.Delegate; import java.io.Closeable; import java.util.Map; @RequiredArgsConstructor @ToString class HeadersAddingDockerHttpClient implements DockerHttpClient { @Delegate(types = Closeable.class) final DockerHttpClient delegate; final Map headers; @Override public Response execute(Request request) { request = Request.builder().from(request).putAllHeaders(headers).build(); return delegate.execute(request); } } ================================================ FILE: core/src/main/java/org/testcontainers/dockerclient/InvalidConfigurationException.java ================================================ package org.testcontainers.dockerclient; /** * Exception to indicate that a {@link DockerClientProviderStrategy} fails. */ public class InvalidConfigurationException extends RuntimeException { public InvalidConfigurationException(String s) { super(s); } public InvalidConfigurationException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: core/src/main/java/org/testcontainers/dockerclient/LogToStringContainerCallback.java ================================================ package org.testcontainers.dockerclient; import com.github.dockerjava.api.async.ResultCallback; import com.github.dockerjava.api.model.Frame; /** * * @deprecated use {@link ResultCallback.Adapter} */ @Deprecated public class LogToStringContainerCallback extends ResultCallback.Adapter { private final StringBuffer log = new StringBuffer(); @Override public void onNext(Frame frame) { log.append(new String(frame.getPayload())); } @Override public String toString() { try { awaitCompletion(); } catch (InterruptedException e) { throw new RuntimeException(e); } return log.toString(); } } ================================================ FILE: core/src/main/java/org/testcontainers/dockerclient/NpipeSocketClientProviderStrategy.java ================================================ package org.testcontainers.dockerclient; import org.apache.commons.lang3.SystemUtils; import java.net.URI; /** * * @deprecated this class is used by the SPI and should not be used directly */ @Deprecated public final class NpipeSocketClientProviderStrategy extends DockerClientProviderStrategy { protected static final String DOCKER_SOCK_PATH = "//./pipe/docker_engine"; private static final String SOCKET_LOCATION = "npipe://" + DOCKER_SOCK_PATH; public static final int PRIORITY = EnvironmentAndSystemPropertyClientProviderStrategy.PRIORITY - 20; @Override public TransportConfig getTransportConfig() { return TransportConfig.builder().dockerHost(URI.create(SOCKET_LOCATION)).build(); } @Override protected boolean isApplicable() { return SystemUtils.IS_OS_WINDOWS; } @Override public String getDescription() { return "local Npipe socket (" + SOCKET_LOCATION + ")"; } @Override protected int getPriority() { return PRIORITY; } } ================================================ FILE: core/src/main/java/org/testcontainers/dockerclient/RootlessDockerClientProviderStrategy.java ================================================ package org.testcontainers.dockerclient; import com.sun.jna.Library; import com.sun.jna.Native; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; import org.jetbrains.annotations.Nullable; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; /** * * @deprecated this class is used by the SPI and should not be used directly */ @Deprecated @Slf4j public final class RootlessDockerClientProviderStrategy extends DockerClientProviderStrategy { public static final int PRIORITY = UnixSocketClientProviderStrategy.PRIORITY + 1; @Getter(lazy = true) @Nullable private final Path socketPath = resolveSocketPath(); private Path resolveSocketPath() { return tryEnv() .orElseGet(() -> { Path homePath = Paths.get(System.getProperty("user.home")).resolve(".docker").resolve("run"); return tryFolder(homePath) .orElseGet(() -> { Path implicitPath = Paths.get("/run/user/" + LibC.INSTANCE.getuid()); return tryFolder(implicitPath).orElse(null); }); }); } private Optional tryEnv() { String xdgRuntimeDir = System.getenv("XDG_RUNTIME_DIR"); if (StringUtils.isBlank(xdgRuntimeDir)) { log.debug("$XDG_RUNTIME_DIR is not set."); return Optional.empty(); } Path path = Paths.get(xdgRuntimeDir); if (!Files.exists(path)) { log.debug("$XDG_RUNTIME_DIR is set to '{}' but the folder does not exist.", path); return Optional.empty(); } Path socketPath = path.resolve("docker.sock"); if (!Files.exists(socketPath)) { log.debug("$XDG_RUNTIME_DIR is set but '{}' does not exist.", socketPath); return Optional.empty(); } return Optional.of(socketPath); } private Optional tryFolder(Path path) { if (!Files.exists(path)) { log.debug("'{}' does not exist.", path); return Optional.empty(); } Path socketPath = path.resolve("docker.sock"); if (!Files.exists(socketPath)) { log.debug("'{}' does not exist.", socketPath); return Optional.empty(); } return Optional.of(socketPath); } @Override public TransportConfig getTransportConfig() throws InvalidConfigurationException { return TransportConfig.builder().dockerHost(URI.create("unix://" + getSocketPath().toString())).build(); } @Override protected boolean isApplicable() { return SystemUtils.IS_OS_LINUX && getSocketPath() != null && Files.exists(getSocketPath()); } @Override public String getDescription() { return "Rootless Docker accessed via Unix socket (" + getSocketPath() + ")"; } @Override protected int getPriority() { return PRIORITY; } private interface LibC extends Library { LibC INSTANCE = Native.loadLibrary("c", LibC.class); int getuid(); } } ================================================ FILE: core/src/main/java/org/testcontainers/dockerclient/TestcontainersHostPropertyClientProviderStrategy.java ================================================ package org.testcontainers.dockerclient; import com.github.dockerjava.core.DefaultDockerClientConfig; import com.github.dockerjava.core.DockerClientConfig; import org.testcontainers.utility.TestcontainersConfiguration; import java.util.Optional; /** * Use tc.host in ~/.testcontainers.properties * to try and locate a docker environment. * * @deprecated this class is used by the SPI and should not be used directly */ @Deprecated public final class TestcontainersHostPropertyClientProviderStrategy extends DockerClientProviderStrategy { public static final int PRIORITY = EnvironmentAndSystemPropertyClientProviderStrategy.PRIORITY - 10; private DockerClientConfig dockerClientConfig; public TestcontainersHostPropertyClientProviderStrategy() { this(DefaultDockerClientConfig.createDefaultConfigBuilder()); } TestcontainersHostPropertyClientProviderStrategy(DefaultDockerClientConfig.Builder configBuilder) { Optional tcHost = Optional.ofNullable( TestcontainersConfiguration.getInstance().getUserProperty("tc.host", null) ); if (tcHost.isPresent()) { configBuilder.withDockerHost(tcHost.get()); this.dockerClientConfig = configBuilder.build(); } } @Override public String getDescription() { return "Testcontainers Host with tc.host=" + this.dockerClientConfig.getDockerHost(); } @Override public TransportConfig getTransportConfig() throws InvalidConfigurationException { return TransportConfig .builder() .dockerHost(dockerClientConfig.getDockerHost()) .sslConfig(dockerClientConfig.getSSLConfig()) .build(); } @Override protected boolean isApplicable() { return this.dockerClientConfig != null; } @Override protected int getPriority() { return PRIORITY; } @Override protected boolean isPersistable() { return false; } @Override public boolean allowUserOverrides() { return false; } } ================================================ FILE: core/src/main/java/org/testcontainers/dockerclient/TransportConfig.java ================================================ package org.testcontainers.dockerclient; import com.github.dockerjava.transport.SSLConfig; import lombok.Builder; import lombok.Value; import org.jetbrains.annotations.Nullable; import java.net.URI; @Builder @Value public class TransportConfig { URI dockerHost; @Nullable SSLConfig sslConfig; } ================================================ FILE: core/src/main/java/org/testcontainers/dockerclient/UnixSocketClientProviderStrategy.java ================================================ package org.testcontainers.dockerclient; import org.apache.commons.lang3.SystemUtils; import java.io.IOException; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; /** * * @deprecated this class is used by the SPI and should not be used directly */ @Deprecated public final class UnixSocketClientProviderStrategy extends DockerClientProviderStrategy { protected static final String DOCKER_SOCK_PATH = "/var/run/docker.sock"; private static final String SOCKET_LOCATION = "unix://" + DOCKER_SOCK_PATH; private static final int SOCKET_FILE_MODE_MASK = 0xc000; public static final int PRIORITY = EnvironmentAndSystemPropertyClientProviderStrategy.PRIORITY - 20; @Override public TransportConfig getTransportConfig() throws InvalidConfigurationException { Path dockerSocketFile = Paths.get(DOCKER_SOCK_PATH); Integer mode; try { mode = (Integer) Files.getAttribute(dockerSocketFile, "unix:mode"); } catch (IOException e) { throw new InvalidConfigurationException("Could not find unix domain socket", e); } if ((mode & 0xc000) != SOCKET_FILE_MODE_MASK) { throw new InvalidConfigurationException( "Found docker unix domain socket but file mode was not as expected (expected: srwxr-xr-x). This problem is possibly due to occurrence of this issue in the past: https://github.com/docker/docker/issues/13121" ); } return TransportConfig.builder().dockerHost(URI.create(SOCKET_LOCATION)).build(); } @Override protected boolean isApplicable() { return SystemUtils.IS_OS_LINUX || SystemUtils.IS_OS_MAC; } @Override public String getDescription() { return "local Unix socket (" + SOCKET_LOCATION + ")"; } @Override protected int getPriority() { return PRIORITY; } } ================================================ FILE: core/src/main/java/org/testcontainers/images/AbstractImagePullPolicy.java ================================================ package org.testcontainers.images; import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.DockerLoggerFactory; @Slf4j public abstract class AbstractImagePullPolicy implements ImagePullPolicy { private static final LocalImagesCache LOCAL_IMAGES_CACHE = LocalImagesCache.INSTANCE; @Override public boolean shouldPull(DockerImageName imageName) { Logger logger = DockerLoggerFactory.getLogger(imageName.asCanonicalNameString()); // Does our cache already know the image? ImageData cachedImageData = LOCAL_IMAGES_CACHE.get(imageName); if (cachedImageData != null) { logger.trace("{} is already in image name cache", imageName); } else { logger.debug("{} is not in image name cache, updating...", imageName); // Was not in cache, inspecting... cachedImageData = LOCAL_IMAGES_CACHE.refreshCache(imageName).orElse(null); if (cachedImageData == null) { log.debug("Not available locally, should pull image: {}", imageName); return true; } } if (shouldPullCached(imageName, cachedImageData)) { log.debug("Should pull locally available image: {}", imageName); return true; } else { log.debug("Using locally available and not pulling image: {}", imageName); return false; } } /** * Implement this method to decide whether a locally available image should be pulled * (e.g. to always pull images, or to pull them after some duration of time) * * @return {@code true} to update the locally available image, {@code false} to use local instead */ protected abstract boolean shouldPullCached(DockerImageName imageName, ImageData localImageData); } ================================================ FILE: core/src/main/java/org/testcontainers/images/AgeBasedPullPolicy.java ================================================ package org.testcontainers.images; import lombok.Value; import lombok.extern.slf4j.Slf4j; import org.testcontainers.utility.DockerImageName; import java.time.Duration; import java.time.Instant; /** * An ImagePullPolicy which pulls the image from a remote repository only if its created date is older than maxAge */ @Slf4j @Value class AgeBasedPullPolicy extends AbstractImagePullPolicy { Duration maxAge; @Override protected boolean shouldPullCached(DockerImageName imageName, ImageData localImageData) { Duration imageAge = Duration.between(localImageData.getCreatedAt(), Instant.now()); boolean result = imageAge.compareTo(maxAge) > 0; if (result) { log.trace("Should pull image: {}", imageName); } return result; } } ================================================ FILE: core/src/main/java/org/testcontainers/images/AlwaysPullPolicy.java ================================================ package org.testcontainers.images; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.testcontainers.utility.DockerImageName; /*** * An ImagePullPolicy which pulls the image even if it exists locally. * Useful for obtaining the latest version of an image with a static tag, i.e. 'latest' */ @Slf4j @ToString class AlwaysPullPolicy implements ImagePullPolicy { @Override public boolean shouldPull(DockerImageName imageName) { log.trace("Unconditionally pulling an image: {}", imageName); return true; } } ================================================ FILE: core/src/main/java/org/testcontainers/images/DefaultPullPolicy.java ================================================ package org.testcontainers.images; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.testcontainers.utility.DockerImageName; /** * The default imagePullPolicy, which pulls the image from a remote repository only if it does not exist locally */ @Slf4j @ToString class DefaultPullPolicy extends AbstractImagePullPolicy { @Override protected boolean shouldPullCached(DockerImageName imageName, ImageData localImageData) { return false; } } ================================================ FILE: core/src/main/java/org/testcontainers/images/ImageData.java ================================================ package org.testcontainers.images; import com.github.dockerjava.api.command.InspectImageResponse; import com.github.dockerjava.api.model.Image; import lombok.Builder; import lombok.NonNull; import lombok.Value; import java.time.Instant; import java.time.ZonedDateTime; @Value @Builder public class ImageData { @NonNull Instant createdAt; static ImageData from(InspectImageResponse inspectImageResponse) { final String created = inspectImageResponse.getCreated(); final Instant createdInstant = ((created == null) || created.isEmpty()) ? Instant.EPOCH : ZonedDateTime.parse(created).toInstant(); return ImageData.builder().createdAt(createdInstant).build(); } static ImageData from(Image image) { final Long created = image.getCreated(); final Instant createdInstant = (created == null) ? Instant.EPOCH : Instant.ofEpochSecond(created); return ImageData.builder().createdAt(createdInstant).build(); } } ================================================ FILE: core/src/main/java/org/testcontainers/images/ImagePullPolicy.java ================================================ package org.testcontainers.images; import org.testcontainers.utility.DockerImageName; public interface ImagePullPolicy { boolean shouldPull(DockerImageName imageName); } ================================================ FILE: core/src/main/java/org/testcontainers/images/LocalImagesCache.java ================================================ package org.testcontainers.images; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.InspectImageResponse; import com.github.dockerjava.api.exception.NotFoundException; import com.github.dockerjava.api.model.Image; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; import org.testcontainers.DockerClientFactory; import org.testcontainers.utility.DockerImageName; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import java.util.stream.Stream; @Slf4j enum LocalImagesCache { INSTANCE; @VisibleForTesting final AtomicBoolean initialized = new AtomicBoolean(false); @VisibleForTesting final Map cache = new ConcurrentHashMap<>(); public ImageData get(DockerImageName imageName) { maybeInitCache(DockerClientFactory.instance().client()); return cache.get(imageName); } public Optional refreshCache(DockerImageName imageName) { DockerClient dockerClient = DockerClientFactory.instance().client(); if (!maybeInitCache(dockerClient)) { // Cache may be stale, trying inspectImageCmd... InspectImageResponse response = null; try { response = dockerClient.inspectImageCmd(imageName.asCanonicalNameString()).exec(); } catch (NotFoundException e) { log.trace("Image {} not found", imageName, e); } if (response != null) { ImageData imageData = ImageData.from(response); cache.put(imageName, imageData); return Optional.of(imageData); } else { cache.remove(imageName); return Optional.empty(); } } return Optional.ofNullable(cache.get(imageName)); } private synchronized boolean maybeInitCache(DockerClient dockerClient) { if (!initialized.compareAndSet(false, true)) { return false; } if (Boolean.parseBoolean(System.getProperty("useFilter"))) { return false; } populateFromList(dockerClient.listImagesCmd().exec()); return true; } private void populateFromList(List images) { for (Image image : images) { String[] repoTags = image.getRepoTags(); if (repoTags == null) { log.debug("repoTags is null, skipping image: {}", image); continue; } cache.putAll( Stream .of(repoTags) // Protection against some edge case where local image repository tags end up with duplicates // making toMap crash at merge time. .distinct() .collect(Collectors.toMap(DockerImageName::new, it -> ImageData.from(image))) ); } } } ================================================ FILE: core/src/main/java/org/testcontainers/images/LoggedPullImageResultCallback.java ================================================ package org.testcontainers.images; import com.github.dockerjava.api.command.PullImageResultCallback; import com.github.dockerjava.api.model.PullResponseItem; import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import java.io.Closeable; import java.time.Duration; import java.time.Instant; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Set; /** * {@link PullImageResultCallback} with improved logging of pull progress. */ class LoggedPullImageResultCallback extends PullImageResultCallback { private final Logger logger; private final Set allLayers = new HashSet<>(); private final Set downloadedLayers = new HashSet<>(); private final Set pulledLayers = new HashSet<>(); private final Map totalSizes = new HashMap<>(); private final Map currentSizes = new HashMap<>(); private boolean completed; private Instant start; LoggedPullImageResultCallback(final Logger logger) { this.logger = logger; } @Override public void onStart(final Closeable stream) { super.onStart(stream); start = Instant.now(); logger.info("Starting to pull image"); } @Override public void onNext(final PullResponseItem item) { super.onNext(item); final String statusLowercase = item.getStatus() != null ? item.getStatus().toLowerCase() : ""; final String id = item.getId(); if (item.getProgressDetail() != null) { allLayers.add(id); } if (statusLowercase.equalsIgnoreCase("download complete")) { downloadedLayers.add(id); } if (statusLowercase.equalsIgnoreCase("pull complete")) { pulledLayers.add(id); } if (item.getProgressDetail() != null) { Long total = item.getProgressDetail().getTotal(); Long current = item.getProgressDetail().getCurrent(); if (total != null && total > totalSizes.getOrDefault(id, 0L)) { totalSizes.put(id, total); } if (current != null && current > currentSizes.getOrDefault(id, 0L)) { currentSizes.put(id, current); } } if (statusLowercase.startsWith("pulling from") || statusLowercase.contains("complete")) { long totalSize = totalLayerSize(); long currentSize = downloadedLayerSize(); int pendingCount = allLayers.size() - downloadedLayers.size(); String friendlyTotalSize; if (pendingCount > 0) { friendlyTotalSize = "? MB"; } else { friendlyTotalSize = FileUtils.byteCountToDisplaySize(totalSize); } logger.info( "Pulling image layers: {} pending, {} downloaded, {} extracted, ({}/{})", String.format("%2d", pendingCount), String.format("%2d", downloadedLayers.size()), String.format("%2d", pulledLayers.size()), FileUtils.byteCountToDisplaySize(currentSize), friendlyTotalSize ); } if (statusLowercase.contains("complete")) { completed = true; } } @Override public void onComplete() { super.onComplete(); final long downloadedLayerSize = downloadedLayerSize(); final long duration = Duration.between(start, Instant.now()).getSeconds(); if (completed) { logger.info( "Pull complete. {} layers, pulled in {}s (downloaded {} at {}/s)", allLayers.size(), duration, FileUtils.byteCountToDisplaySize(downloadedLayerSize), FileUtils.byteCountToDisplaySize(downloadedLayerSize / duration) ); } } private long downloadedLayerSize() { return currentSizes.values().stream().filter(Objects::nonNull).mapToLong(it -> it).sum(); } private long totalLayerSize() { return totalSizes.values().stream().filter(Objects::nonNull).mapToLong(it -> it).sum(); } } ================================================ FILE: core/src/main/java/org/testcontainers/images/ParsedDockerfile.java ================================================ package org.testcontainers.images; import com.google.common.annotations.VisibleForTesting; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; /** * Representation of a Dockerfile, with partial parsing for extraction of a minimal set of data. */ @Slf4j public class ParsedDockerfile { private static final Pattern FROM_LINE_PATTERN = Pattern.compile( "FROM (?--[^\\s]+\\s)*(?[^\\s]+).*", Pattern.CASE_INSENSITIVE ); private final Path dockerFilePath; @Getter private final Set dependencyImageNames; public ParsedDockerfile(Path dockerFilePath) { this.dockerFilePath = dockerFilePath; this.dependencyImageNames = parse(read()); } @VisibleForTesting ParsedDockerfile(List lines) { this.dockerFilePath = Paths.get("dummy.Dockerfile"); this.dependencyImageNames = parse(lines); } private List read() { if (!Files.exists(dockerFilePath)) { log.warn("Tried to parse Dockerfile at path {} but none was found", dockerFilePath); return Collections.emptyList(); } try { return Files.readAllLines(dockerFilePath); } catch (IOException e) { log.warn("Unable to read Dockerfile at path {}", dockerFilePath, e); return Collections.emptyList(); } } private Set parse(List lines) { Set imageNames = lines .stream() .map(FROM_LINE_PATTERN::matcher) .filter(Matcher::matches) .map(matcher -> matcher.group("image")) .collect(Collectors.toSet()); if (!imageNames.isEmpty()) { log.debug("Found dependency images in Dockerfile {}: {}", dockerFilePath, imageNames); } return imageNames; } } ================================================ FILE: core/src/main/java/org/testcontainers/images/PullPolicy.java ================================================ package org.testcontainers.images; import com.google.common.annotations.VisibleForTesting; import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; import org.testcontainers.utility.TestcontainersConfiguration; import java.time.Duration; /** * Convenience class with logic for building common {@link ImagePullPolicy} instances. * */ @Slf4j @UtilityClass public class PullPolicy { @VisibleForTesting static ImagePullPolicy instance; @VisibleForTesting static ImagePullPolicy defaultImplementation = new DefaultPullPolicy(); /** * Convenience method for returning the {@link DefaultPullPolicy} default image pull policy * @return {@link ImagePullPolicy} */ public static synchronized ImagePullPolicy defaultPolicy() { if (instance != null) { return instance; } String imagePullPolicyClassName = TestcontainersConfiguration.getInstance().getImagePullPolicy(); if (imagePullPolicyClassName != null) { log.debug("Attempting to instantiate an ImagePullPolicy with class: {}", imagePullPolicyClassName); ImagePullPolicy configuredInstance; try { configuredInstance = (ImagePullPolicy) Thread .currentThread() .getContextClassLoader() .loadClass(imagePullPolicyClassName) .getDeclaredConstructor() .newInstance(); } catch (Exception e) { throw new IllegalArgumentException( "Configured ImagePullPolicy could not be loaded: " + imagePullPolicyClassName, e ); } log.info("Found configured Image Pull Policy: {}", configuredInstance.getClass()); instance = configuredInstance; } else { instance = defaultImplementation; } log.info("Image pull policy will be performed by: {}", instance); return instance; } /** * Convenience method for returning the {@link AlwaysPullPolicy} alwaysPull image pull policy * @return {@link ImagePullPolicy} */ public static ImagePullPolicy alwaysPull() { return new AlwaysPullPolicy(); } /** * Convenience method for returning an {@link AgeBasedPullPolicy} Age based image pull policy, * @return {@link ImagePullPolicy} */ public static ImagePullPolicy ageBased(Duration maxAge) { return new AgeBasedPullPolicy(maxAge); } } ================================================ FILE: core/src/main/java/org/testcontainers/images/RemoteDockerImage.java ================================================ package org.testcontainers.images; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.PullImageCmd; import com.github.dockerjava.api.exception.DockerClientException; import com.github.dockerjava.api.exception.InternalServerErrorException; import com.github.dockerjava.api.exception.NotFoundException; import com.google.common.util.concurrent.Futures; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.NonNull; import lombok.SneakyThrows; import lombok.ToString; import lombok.With; import org.awaitility.Awaitility; import org.awaitility.pollinterval.IterativePollInterval; import org.awaitility.pollinterval.PollInterval; import org.slf4j.Logger; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.ContainerFetchException; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.DockerLoggerFactory; import org.testcontainers.utility.ImageNameSubstitutor; import org.testcontainers.utility.LazyFuture; import org.testcontainers.utility.TestcontainersConfiguration; import java.time.Duration; import java.time.Instant; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicReference; @ToString @AllArgsConstructor(access = AccessLevel.PACKAGE) public class RemoteDockerImage extends LazyFuture { private static final Duration PULL_RETRY_TIME_LIMIT = Duration.ofSeconds( TestcontainersConfiguration.getInstance().getImagePullTimeout() ); @ToString.Exclude private Future imageNameFuture; @With ImagePullPolicy imagePullPolicy = PullPolicy.defaultPolicy(); @With private ImageNameSubstitutor imageNameSubstitutor = ImageNameSubstitutor.instance(); @ToString.Exclude private DockerClient dockerClient = DockerClientFactory.lazyClient(); public RemoteDockerImage(DockerImageName dockerImageName) { this.imageNameFuture = CompletableFuture.completedFuture(dockerImageName); } @Deprecated public RemoteDockerImage(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } @Deprecated public RemoteDockerImage(@NonNull String repository, @NonNull String tag) { this(DockerImageName.parse(repository).withTag(tag)); } public RemoteDockerImage(@NonNull Future imageFuture) { this.imageNameFuture = Futures.lazyTransform(imageFuture, DockerImageName::new); } @Override @SneakyThrows({ InterruptedException.class, ExecutionException.class }) protected final String resolve() { final DockerImageName imageName = getImageName(); final Logger logger = DockerLoggerFactory.getLogger(imageName.toString()); try { if (!imagePullPolicy.shouldPull(imageName)) { return imageName.asCanonicalNameString(); } // The image is not available locally - pull it logger.info( "Pulling docker image: {}. Please be patient; this may take some time but only needs to be done once.", imageName ); final Instant startedAt = Instant.now(); final Instant lastRetryAllowed = Instant.now().plus(PULL_RETRY_TIME_LIMIT); final AtomicReference lastFailure = new AtomicReference<>(); final PullImageCmd pullImageCmd = dockerClient .pullImageCmd(imageName.getUnversionedPart()) .withTag(imageName.getVersionPart()); final AtomicReference dockerImageName = new AtomicReference<>(); // The following poll interval in ms: 50, 100, 200, 400, 800.... // Results in ~70 requests in over 2 minutes final PollInterval interval = IterativePollInterval .iterative(duration -> duration.multipliedBy(2)) .startDuration(Duration.ofMillis(50)); Awaitility .await() .pollInSameThread() .pollDelay(Duration.ZERO) // start checking immediately .atMost(PULL_RETRY_TIME_LIMIT) .pollInterval(interval) .until( tryImagePullCommand(pullImageCmd, logger, dockerImageName, imageName, lastFailure, lastRetryAllowed) ); if (dockerImageName.get() == null) { final Exception lastException = lastFailure.get(); logger.error( "Failed to pull image: {}. Please check output of `docker pull {}`", imageName, imageName, lastException ); throw new ContainerFetchException("Failed to pull image: " + imageName, lastException); } logger.info("Image {} pull took {}", dockerImageName.get(), Duration.between(startedAt, Instant.now())); LocalImagesCache.INSTANCE.refreshCache(imageName); return dockerImageName.get(); } catch (DockerClientException e) { throw new ContainerFetchException("Failed to get Docker client for " + imageName, e); } } private Callable tryImagePullCommand( PullImageCmd pullImageCmd, Logger logger, AtomicReference dockerImageName, DockerImageName imageName, AtomicReference lastFailure, Instant lastRetryAllowed ) { return () -> { try { pullImage(pullImageCmd, logger); dockerImageName.set(imageName.asCanonicalNameString()); return true; } catch (InterruptedException | InternalServerErrorException e) { // these classes of exception often relate to timeout/connection errors so should be retried lastFailure.set(e); logger.warn( "Retrying pull for image: {} ({}s remaining)", imageName, Duration.between(Instant.now(), lastRetryAllowed).getSeconds() ); return false; } }; } private TimeLimitedLoggedPullImageResultCallback pullImage(PullImageCmd pullImageCmd, Logger logger) throws InterruptedException { try { return pullImageCmd.exec(new TimeLimitedLoggedPullImageResultCallback(logger)).awaitCompletion(); } catch (DockerClientException | NotFoundException e) { // Try to fallback to x86 return pullImageCmd .withPlatform("linux/amd64") .exec(new TimeLimitedLoggedPullImageResultCallback(logger)) .awaitCompletion(); } } private DockerImageName getImageName() throws InterruptedException, ExecutionException { final DockerImageName specifiedImageName = imageNameFuture.get(); // Allow the image name to be substituted return imageNameSubstitutor.apply(specifiedImageName); } @ToString.Include(name = "imageName", rank = 1) private String imageNameToString() { if (!imageNameFuture.isDone()) { return ""; } try { return getImageName().asCanonicalNameString(); } catch (InterruptedException | ExecutionException e) { return e.getMessage(); } } } ================================================ FILE: core/src/main/java/org/testcontainers/images/TimeLimitedLoggedPullImageResultCallback.java ================================================ package org.testcontainers.images; import com.github.dockerjava.api.command.PullImageResultCallback; import com.github.dockerjava.api.model.PullResponseItem; import org.slf4j.Logger; import org.testcontainers.DockerClientFactory; import org.testcontainers.utility.TestcontainersConfiguration; import java.io.Closeable; import java.io.IOException; import java.time.Duration; import java.util.HashSet; import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** * {@link PullImageResultCallback} with improved logging of pull progress and a 'watchdog' which will abort the pull * if progress is not being made, to prevent a hanging test */ public class TimeLimitedLoggedPullImageResultCallback extends LoggedPullImageResultCallback { private static final AtomicInteger THREAD_ID = new AtomicInteger(0); private static final ScheduledExecutorService PROGRESS_WATCHDOG_EXECUTOR = Executors.newScheduledThreadPool( 0, runnable -> { Thread t = new Thread(DockerClientFactory.TESTCONTAINERS_THREAD_GROUP, runnable); t.setDaemon(true); t.setName("testcontainers-pull-watchdog-" + THREAD_ID.incrementAndGet()); return t; } ); private static final Duration PULL_PAUSE_TOLERANCE = Duration.ofSeconds( TestcontainersConfiguration.getInstance().getImagePullPauseTimeout() ); private final Logger logger; // A future which, if it ever fires, will kill the pull private ScheduledFuture nextCheckForProgress; // All threads that are 'awaiting' this pull private final Set waitingThreads = new HashSet<>(); public TimeLimitedLoggedPullImageResultCallback(Logger logger) { super(logger); this.logger = logger; } @Override public TimeLimitedLoggedPullImageResultCallback awaitCompletion() throws InterruptedException { waitingThreads.add(Thread.currentThread()); super.awaitCompletion(); return this; } @Override public boolean awaitCompletion(long timeout, TimeUnit timeUnit) throws InterruptedException { waitingThreads.add(Thread.currentThread()); return super.awaitCompletion(timeout, timeUnit); } @Override public void onNext(PullResponseItem item) { if (item.getProgressDetail() != null) { resetProgressWatchdog(false); } super.onNext(item); } @Override public void onStart(Closeable stream) { resetProgressWatchdog(false); super.onStart(stream); } @Override public void onError(Throwable throwable) { resetProgressWatchdog(true); super.onError(throwable); } @Override public void onComplete() { resetProgressWatchdog(true); super.onComplete(); } /* * This method schedules a future task which will interrupt the waiting waiting threads if ever fired. * Every time this method is called (from onStart or onNext), the task is cancelled and recreated 30s in the future, * ensuring that it will only fire if the method stops being called regularly (e.g. if the pull has hung). */ private void resetProgressWatchdog(boolean isFinished) { if (nextCheckForProgress != null && !nextCheckForProgress.isCancelled()) { nextCheckForProgress.cancel(false); } if (!isFinished) { nextCheckForProgress = PROGRESS_WATCHDOG_EXECUTOR.schedule( this::abortPull, PULL_PAUSE_TOLERANCE.getSeconds(), TimeUnit.SECONDS ); } } private void abortPull() { logger.error( "Docker image pull has not made progress in {}s - aborting pull", PULL_PAUSE_TOLERANCE.getSeconds() ); // Interrupt any threads that are waiting, before closing streams, because the stream can take // an indeterminate amount of time to close waitingThreads.forEach(Thread::interrupt); try { close(); } catch (IOException ignored) { // no action } } } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/ImageFromDockerfile.java ================================================ package org.testcontainers.images.builder; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.BuildImageCmd; import com.github.dockerjava.api.command.BuildImageResultCallback; import com.github.dockerjava.api.model.BuildResponseItem; import lombok.Cleanup; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.testcontainers.DockerClientFactory; import org.testcontainers.images.ParsedDockerfile; import org.testcontainers.images.RemoteDockerImage; import org.testcontainers.images.builder.traits.BuildContextBuilderTrait; import org.testcontainers.images.builder.traits.ClasspathTrait; import org.testcontainers.images.builder.traits.DockerfileTrait; import org.testcontainers.images.builder.traits.FilesTrait; import org.testcontainers.images.builder.traits.StringsTrait; import org.testcontainers.utility.Base58; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.DockerLoggerFactory; import org.testcontainers.utility.ImageNameSubstitutor; import org.testcontainers.utility.LazyFuture; import org.testcontainers.utility.ResourceReaper; import java.io.IOException; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.nio.file.Path; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.zip.GZIPOutputStream; @Slf4j @Getter public class ImageFromDockerfile extends LazyFuture implements BuildContextBuilderTrait, ClasspathTrait, FilesTrait, StringsTrait, DockerfileTrait { private final String dockerImageName; private boolean deleteOnExit = true; private final Map transferables = new HashMap<>(); private final Map buildArgs = new HashMap<>(); private Optional dockerFilePath = Optional.empty(); private Optional dockerfile = Optional.empty(); private Optional target = Optional.empty(); private final Set> buildImageCmdModifiers = new LinkedHashSet<>(); private Set dependencyImageNames = Collections.emptySet(); public ImageFromDockerfile() { this("localhost/testcontainers/" + Base58.randomString(16).toLowerCase()); } public ImageFromDockerfile(String dockerImageName) { this(dockerImageName, true); } public ImageFromDockerfile(String dockerImageName, boolean deleteOnExit) { this.dockerImageName = dockerImageName; this.deleteOnExit = deleteOnExit; } @Override public ImageFromDockerfile withFileFromTransferable(String path, Transferable transferable) { Transferable oldValue = transferables.put(path, transferable); if (oldValue != null) { log.warn("overriding previous mapping for '{}'", path); } return this; } @Override protected final String resolve() { Logger logger = DockerLoggerFactory.getLogger(dockerImageName); //noinspection resource DockerClient dockerClient = DockerClientFactory.instance().client(); try { BuildImageResultCallback resultCallback = new BuildImageResultCallback() { @Override public void onNext(BuildResponseItem item) { super.onNext(item); if (item.isErrorIndicated()) { logger.error(item.getErrorDetail().getMessage()); } else { logger.debug(StringUtils.removeEnd(item.getStream(), "\n")); } } }; // We have to use pipes to avoid high memory consumption since users might want to build huge images @Cleanup PipedInputStream in = new PipedInputStream(); @Cleanup PipedOutputStream out = new PipedOutputStream(in); BuildImageCmd buildImageCmd = dockerClient.buildImageCmd(in); configure(buildImageCmd); Map labels = new HashMap<>(); if (buildImageCmd.getLabels() != null) { labels.putAll(buildImageCmd.getLabels()); } labels.putAll(DockerClientFactory.DEFAULT_LABELS); if (deleteOnExit) { //noinspection deprecation labels.putAll(ResourceReaper.instance().getLabels()); } buildImageCmd.withLabels(labels); prePullDependencyImages(dependencyImageNames); BuildImageResultCallback exec = buildImageCmd.exec(resultCallback); long bytesToDockerDaemon = 0; // To build an image, we have to send the context to Docker in TAR archive format try (TarArchiveOutputStream tarArchive = new TarArchiveOutputStream(new GZIPOutputStream(out))) { tarArchive.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); tarArchive.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX); for (Map.Entry entry : transferables.entrySet()) { Transferable transferable = entry.getValue(); final String destination = entry.getKey(); transferable.transferTo(tarArchive, destination); bytesToDockerDaemon += transferable.getSize(); } tarArchive.finish(); } log.info("Transferred {} to Docker daemon", FileUtils.byteCountToDisplaySize(bytesToDockerDaemon)); if (bytesToDockerDaemon > FileUtils.ONE_MB * 50) { log.warn( // warn if >50MB sent to docker daemon "A large amount of data was sent to the Docker daemon ({}). Consider using a .dockerignore file for better performance.", FileUtils.byteCountToDisplaySize(bytesToDockerDaemon) ); } exec.awaitImageId(); return dockerImageName; } catch (IOException e) { throw new RuntimeException("Can't close DockerClient", e); } } protected void configure(BuildImageCmd buildImageCmd) { buildImageCmd.withTags(Collections.singleton(getDockerImageName())); this.dockerFilePath.ifPresent(buildImageCmd::withDockerfilePath); this.dockerfile.ifPresent(p -> { buildImageCmd.withDockerfile(p.toFile()); dependencyImageNames = new ParsedDockerfile(p).getDependencyImageNames(); if (dependencyImageNames.size() > 0) { // if we'll be pre-pulling images, disable the built-in pull as it is not necessary and will fail for // authenticated registries buildImageCmd.withPull(false); } }); this.buildArgs.forEach(buildImageCmd::withBuildArg); this.target.ifPresent(buildImageCmd::withTarget); this.buildImageCmdModifiers.forEach(hook -> hook.accept(buildImageCmd)); } private void prePullDependencyImages(Set imagesToPull) { imagesToPull.forEach(imageName -> { String resolvedImageName = applyBuildArgsToImageName(imageName); try { log.info( "Pre-emptively checking local images for '{}', referenced via a Dockerfile. If not available, it will be pulled.", resolvedImageName ); new RemoteDockerImage(DockerImageName.parse(resolvedImageName)) .withImageNameSubstitutor(ImageNameSubstitutor.noop()) .get(); } catch (Exception e) { log.warn( "Unable to pre-fetch an image ({}) depended upon by Dockerfile - image build will continue but may fail. Exception message was: {}", resolvedImageName, e.getMessage() ); } }); } /** * See {@code filterForEnvironmentVars()} in {@link com.github.dockerjava.core.dockerfile.DockerfileStatement}. */ private String applyBuildArgsToImageName(String imageName) { for (Map.Entry entry : buildArgs.entrySet()) { String value = Matcher.quoteReplacement(entry.getValue()); // handle: $VARIABLE case imageName = imageName.replace("$" + entry.getKey(), value); // handle ${VARIABLE} case imageName = imageName.replace("${" + entry.getKey() + "}", value); } return imageName; } public ImageFromDockerfile withBuildArg(final String key, final String value) { this.buildArgs.put(key, value); return this; } public ImageFromDockerfile withBuildArgs(final Map args) { this.buildArgs.putAll(args); return this; } /** * Sets the target build stage to use. * * @param target the target build stage */ public ImageFromDockerfile withTarget(String target) { this.target = Optional.of(target); return this; } /** * Sets the Dockerfile to be used for this image. * * @param relativePathFromBuildContextDirectory relative path to the Dockerfile, relative to the image build context directory * @deprecated It is recommended to use {@link #withDockerfile} instead because it honors .dockerignore files and * will therefore be more efficient. Additionally, using {@link #withDockerfile} supports Dockerfiles that depend * upon images from authenticated private registries. */ @Deprecated public ImageFromDockerfile withDockerfilePath(String relativePathFromBuildContextDirectory) { this.dockerFilePath = Optional.of(relativePathFromBuildContextDirectory); return this; } /** * Sets the Dockerfile to be used for this image. Honors .dockerignore files for efficiency. * Additionally, supports Dockerfiles that depend upon images from authenticated private registries. * * @param dockerfile path to Dockerfile on the test host. */ public ImageFromDockerfile withDockerfile(Path dockerfile) { this.dockerfile = Optional.of(dockerfile); return this; } /** * Allow low level modifications of {@link BuildImageCmd}. * Warning: this does expose the underlying docker-java API so might change outside of our control. * * @param modifier {@link Consumer} of {@link BuildImageCmd}. * @return this */ public ImageFromDockerfile withBuildImageCmdModifier(Consumer modifier) { this.buildImageCmdModifiers.add(modifier); return this; } } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/Transferable.java ================================================ package org.testcontainers.images.builder; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.apache.commons.io.IOUtils; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.zip.Checksum; public interface Transferable { int DEFAULT_FILE_MODE = 0100644; int DEFAULT_DIR_MODE = 040755; static Transferable of(String string) { return of(string.getBytes(StandardCharsets.UTF_8)); } static Transferable of(String string, int fileMode) { return of(string.getBytes(StandardCharsets.UTF_8), fileMode); } static Transferable of(byte[] bytes) { return of(bytes, DEFAULT_FILE_MODE); } static Transferable of(byte[] bytes, int fileMode) { return new Transferable() { @Override public long getSize() { return bytes.length; } @Override public byte[] getBytes() { return bytes; } @Override public void updateChecksum(Checksum checksum) { checksum.update(bytes, 0, bytes.length); } @Override public int getFileMode() { return fileMode; } }; } /** * Get file mode. Default is 0100644. * * @return file mode * @see Transferable#DEFAULT_FILE_MODE */ default int getFileMode() { return DEFAULT_FILE_MODE; } /** * Size of an object. * * @return size in bytes */ long getSize(); /** * transfer content of this Transferable to the output stream. Must not close the stream. * * @param tarArchiveOutputStream stream to output * @param destination */ default void transferTo(TarArchiveOutputStream tarArchiveOutputStream, final String destination) { TarArchiveEntry tarEntry = new TarArchiveEntry(destination); tarEntry.setSize(getSize()); tarEntry.setMode(getFileMode()); try { tarArchiveOutputStream.putArchiveEntry(tarEntry); IOUtils.write(getBytes(), tarArchiveOutputStream); tarArchiveOutputStream.closeArchiveEntry(); } catch (IOException e) { throw new RuntimeException("Can't transfer " + getDescription(), e); } } default byte[] getBytes() { return new byte[0]; } default String getDescription() { return ""; } default void updateChecksum(Checksum checksum) { throw new UnsupportedOperationException("Provide implementation in subclass"); } } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/dockerfile/DockerfileBuilder.java ================================================ package org.testcontainers.images.builder.dockerfile; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.testcontainers.images.builder.dockerfile.statement.Statement; import org.testcontainers.images.builder.dockerfile.traits.AddStatementTrait; import org.testcontainers.images.builder.dockerfile.traits.CmdStatementTrait; import org.testcontainers.images.builder.dockerfile.traits.CopyStatementTrait; import org.testcontainers.images.builder.dockerfile.traits.DockerfileBuilderTrait; import org.testcontainers.images.builder.dockerfile.traits.EntryPointStatementTrait; import org.testcontainers.images.builder.dockerfile.traits.EnvStatementTrait; import org.testcontainers.images.builder.dockerfile.traits.ExposeStatementTrait; import org.testcontainers.images.builder.dockerfile.traits.FromStatementTrait; import org.testcontainers.images.builder.dockerfile.traits.LabelStatementTrait; import org.testcontainers.images.builder.dockerfile.traits.RunStatementTrait; import org.testcontainers.images.builder.dockerfile.traits.UserStatementTrait; import org.testcontainers.images.builder.dockerfile.traits.VolumeStatementTrait; import org.testcontainers.images.builder.dockerfile.traits.WorkdirStatementTrait; import java.util.ArrayList; import java.util.List; @Data @Slf4j public class DockerfileBuilder implements DockerfileBuilderTrait, FromStatementTrait, AddStatementTrait, CopyStatementTrait, RunStatementTrait, CmdStatementTrait, WorkdirStatementTrait, EnvStatementTrait, LabelStatementTrait, ExposeStatementTrait, EntryPointStatementTrait, VolumeStatementTrait, UserStatementTrait { private final List statements = new ArrayList<>(); public String build() { StringBuilder builder = new StringBuilder(); for (Statement statement : statements) { builder.append(statement.getType()); builder.append(" "); statement.appendArguments(builder); builder.append("\n"); } String result = builder.toString(); log.debug("Returning Dockerfile:\n{}", result); return result; } } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/dockerfile/statement/KeyValuesStatement.java ================================================ package org.testcontainers.images.builder.dockerfile.statement; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Iterator; import java.util.Map; import java.util.Set; public class KeyValuesStatement extends Statement { private static final ObjectMapper objectMapper = new ObjectMapper(); protected final Map entries; public KeyValuesStatement(String type, Map entries) { super(type); this.entries = entries; } @Override public void appendArguments(StringBuilder dockerfileStringBuilder) { Set> entries = this.entries.entrySet(); Iterator> iterator = entries.iterator(); while (iterator.hasNext()) { Map.Entry entry = iterator.next(); try { dockerfileStringBuilder.append(objectMapper.writeValueAsString(entry.getKey())); dockerfileStringBuilder.append("="); dockerfileStringBuilder.append(objectMapper.writeValueAsString(entry.getValue())); } catch (JsonProcessingException e) { throw new RuntimeException("Can't serialize entry: " + entry, e); } if (iterator.hasNext()) { dockerfileStringBuilder.append(" \\\n\t"); } } } } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/dockerfile/statement/MultiArgsStatement.java ================================================ package org.testcontainers.images.builder.dockerfile.statement; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Arrays; public class MultiArgsStatement extends Statement { private static final ObjectMapper objectMapper = new ObjectMapper(); protected final String[] args; public MultiArgsStatement(String type, String... args) { super(type); this.args = args; } @Override public void appendArguments(StringBuilder dockerfileStringBuilder) { try { dockerfileStringBuilder.append(objectMapper.writeValueAsString(args)); } catch (JsonProcessingException e) { throw new RuntimeException("Can't serialize arguments: " + Arrays.toString(args), e); } } } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/dockerfile/statement/RawStatement.java ================================================ package org.testcontainers.images.builder.dockerfile.statement; public class RawStatement extends Statement { final String rawValue; public RawStatement(String type, String rawValue) { super(type); this.rawValue = rawValue; } @Override public void appendArguments(StringBuilder dockerfileStringBuilder) { dockerfileStringBuilder.append(rawValue); } } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/dockerfile/statement/SingleArgumentStatement.java ================================================ package org.testcontainers.images.builder.dockerfile.statement; public class SingleArgumentStatement extends Statement { protected final String argument; public SingleArgumentStatement(String type, String argument) { super(type); this.argument = argument; } @Override public void appendArguments(StringBuilder dockerfileStringBuilder) { dockerfileStringBuilder.append(argument.replace("\n", "\\\n")); } } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/dockerfile/statement/Statement.java ================================================ package org.testcontainers.images.builder.dockerfile.statement; import lombok.Data; @Data public abstract class Statement { final String type; public abstract void appendArguments(StringBuilder dockerfileStringBuilder); } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/dockerfile/traits/AddStatementTrait.java ================================================ package org.testcontainers.images.builder.dockerfile.traits; import org.testcontainers.images.builder.dockerfile.statement.MultiArgsStatement; public interface AddStatementTrait & DockerfileBuilderTrait> { default SELF add(String source, String destination) { return ((SELF) this).withStatement(new MultiArgsStatement("ADD", source, destination)); } } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/dockerfile/traits/CmdStatementTrait.java ================================================ package org.testcontainers.images.builder.dockerfile.traits; import org.testcontainers.images.builder.dockerfile.statement.MultiArgsStatement; import org.testcontainers.images.builder.dockerfile.statement.SingleArgumentStatement; public interface CmdStatementTrait & DockerfileBuilderTrait> { default SELF cmd(String command) { return ((SELF) this).withStatement(new SingleArgumentStatement("CMD", command)); } default SELF cmd(String... commandParts) { return ((SELF) this).withStatement(new MultiArgsStatement("CMD", commandParts)); } } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/dockerfile/traits/CopyStatementTrait.java ================================================ package org.testcontainers.images.builder.dockerfile.traits; import org.testcontainers.images.builder.dockerfile.statement.MultiArgsStatement; public interface CopyStatementTrait & DockerfileBuilderTrait> { default SELF copy(String source, String destination) { return ((SELF) this).withStatement(new MultiArgsStatement("COPY", source, destination)); } } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/dockerfile/traits/DockerfileBuilderTrait.java ================================================ package org.testcontainers.images.builder.dockerfile.traits; import org.testcontainers.images.builder.dockerfile.statement.Statement; import java.util.List; public interface DockerfileBuilderTrait> { List getStatements(); default SELF withStatement(Statement statement) { getStatements().add(statement); return (SELF) this; } } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/dockerfile/traits/EntryPointStatementTrait.java ================================================ package org.testcontainers.images.builder.dockerfile.traits; import org.testcontainers.images.builder.dockerfile.statement.MultiArgsStatement; import org.testcontainers.images.builder.dockerfile.statement.SingleArgumentStatement; public interface EntryPointStatementTrait & DockerfileBuilderTrait> { default SELF entryPoint(String command) { return ((SELF) this).withStatement(new SingleArgumentStatement("ENTRYPOINT", command)); } default SELF entryPoint(String... commandParts) { return ((SELF) this).withStatement(new MultiArgsStatement("ENTRYPOINT", commandParts)); } } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/dockerfile/traits/EnvStatementTrait.java ================================================ package org.testcontainers.images.builder.dockerfile.traits; import org.testcontainers.images.builder.dockerfile.statement.KeyValuesStatement; import java.util.Collections; import java.util.Map; public interface EnvStatementTrait & DockerfileBuilderTrait> { default SELF env(String key, String value) { return env(Collections.singletonMap(key, value)); } default SELF env(Map entries) { return ((SELF) this).withStatement(new KeyValuesStatement("ENV", entries)); } } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/dockerfile/traits/ExposeStatementTrait.java ================================================ package org.testcontainers.images.builder.dockerfile.traits; import org.testcontainers.images.builder.dockerfile.statement.SingleArgumentStatement; import java.util.stream.Collectors; import java.util.stream.Stream; public interface ExposeStatementTrait & DockerfileBuilderTrait> { default SELF expose(Integer... ports) { return ((SELF) this).withStatement( new SingleArgumentStatement( "EXPOSE", Stream.of(ports).map(Object::toString).collect(Collectors.joining(" ")) ) ); } } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/dockerfile/traits/FromStatementTrait.java ================================================ package org.testcontainers.images.builder.dockerfile.traits; import org.testcontainers.images.builder.dockerfile.statement.SingleArgumentStatement; import org.testcontainers.utility.DockerImageName; public interface FromStatementTrait & DockerfileBuilderTrait> { default SELF from(String dockerImageName) { DockerImageName.parse(dockerImageName).assertValid(); return ((SELF) this).withStatement(new SingleArgumentStatement("FROM", dockerImageName)); } } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/dockerfile/traits/LabelStatementTrait.java ================================================ package org.testcontainers.images.builder.dockerfile.traits; import org.testcontainers.images.builder.dockerfile.statement.KeyValuesStatement; import java.util.Collections; import java.util.Map; public interface LabelStatementTrait & DockerfileBuilderTrait> { default SELF label(String key, String value) { return label(Collections.singletonMap(key, value)); } default SELF label(Map entries) { return ((SELF) this).withStatement(new KeyValuesStatement("LABEL", entries)); } } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/dockerfile/traits/RunStatementTrait.java ================================================ package org.testcontainers.images.builder.dockerfile.traits; import org.testcontainers.images.builder.dockerfile.statement.MultiArgsStatement; import org.testcontainers.images.builder.dockerfile.statement.SingleArgumentStatement; public interface RunStatementTrait & DockerfileBuilderTrait> { default SELF run(String... commandParts) { return ((SELF) this).withStatement(new MultiArgsStatement("RUN", commandParts)); } default SELF run(String command) { return ((SELF) this).withStatement(new SingleArgumentStatement("RUN", command)); } } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/dockerfile/traits/UserStatementTrait.java ================================================ package org.testcontainers.images.builder.dockerfile.traits; import org.testcontainers.images.builder.dockerfile.statement.SingleArgumentStatement; public interface UserStatementTrait & DockerfileBuilderTrait> { default SELF user(String user) { return ((SELF) this).withStatement(new SingleArgumentStatement("USER", user)); } } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/dockerfile/traits/VolumeStatementTrait.java ================================================ package org.testcontainers.images.builder.dockerfile.traits; import org.testcontainers.images.builder.dockerfile.statement.MultiArgsStatement; public interface VolumeStatementTrait & DockerfileBuilderTrait> { default SELF volume(String... volumes) { return ((SELF) this).withStatement(new MultiArgsStatement("VOLUME", volumes)); } } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/dockerfile/traits/WorkdirStatementTrait.java ================================================ package org.testcontainers.images.builder.dockerfile.traits; import org.testcontainers.images.builder.dockerfile.statement.SingleArgumentStatement; public interface WorkdirStatementTrait & DockerfileBuilderTrait> { default SELF workDir(String workdir) { return ((SELF) this).withStatement(new SingleArgumentStatement("WORKDIR", workdir)); } } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/traits/BuildContextBuilderTrait.java ================================================ package org.testcontainers.images.builder.traits; import org.testcontainers.images.builder.Transferable; /** * base BuildContextBuilder's trait * */ public interface BuildContextBuilderTrait> { SELF withFileFromTransferable(String path, Transferable transferable); } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/traits/ClasspathTrait.java ================================================ package org.testcontainers.images.builder.traits; import org.testcontainers.utility.MountableFile; import java.nio.file.Paths; /** * BuildContextBuilder's trait for classpath-based resources. * */ public interface ClasspathTrait & BuildContextBuilderTrait & FilesTrait> { default SELF withFileFromClasspath(String path, String resourcePath) { final MountableFile mountableFile = MountableFile.forClasspathResource(resourcePath); return ((SELF) this).withFileFromPath(path, Paths.get(mountableFile.getResolvedPath())); } } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/traits/DockerfileTrait.java ================================================ package org.testcontainers.images.builder.traits; import lombok.Getter; import org.testcontainers.images.builder.Transferable; import org.testcontainers.images.builder.dockerfile.DockerfileBuilder; import java.util.function.Consumer; /** * BuildContextBuilder's trait for Dockerfile-based resources. * */ public interface DockerfileTrait< SELF extends DockerfileTrait & BuildContextBuilderTrait & StringsTrait > { default SELF withDockerfileFromBuilder(Consumer builderConsumer) { DockerfileBuilder builder = new DockerfileBuilder(); builderConsumer.accept(builder); // return Transferable because we want to build Dockerfile's content lazily return ((SELF) this).withFileFromTransferable( "Dockerfile", new Transferable() { @Getter(lazy = true) private final byte[] bytes = builder.build().getBytes(); @Override public long getSize() { return getBytes().length; } @Override public String getDescription() { return "Dockerfile: " + builder; } } ); } } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/traits/FilesTrait.java ================================================ package org.testcontainers.images.builder.traits; import org.testcontainers.utility.MountableFile; import java.io.File; import java.nio.file.Path; /** * BuildContextBuilder's trait for NIO-based (Files and Paths) manipulations. * */ public interface FilesTrait & BuildContextBuilderTrait> { /** * Adds file to tarball copied into container. * @param path in tarball * @param file in host filesystem * @return self */ default SELF withFileFromFile(String path, File file) { return withFileFromPath(path, file.toPath(), null); } /** * Adds file to tarball copied into container. * @param path in tarball * @param filePath in host filesystem * @return self */ default SELF withFileFromPath(String path, Path filePath) { return withFileFromPath(path, filePath, null); } /** * Adds file with given mode to tarball copied into container. * @param path in tarball * @param file in host filesystem * @param mode octal value of posix file mode (000..777) * @return self */ default SELF withFileFromFile(String path, File file, Integer mode) { return withFileFromPath(path, file.toPath(), mode); } /** * Adds file with given mode to tarball copied into container. * @param path in tarball * @param filePath in host filesystem * @param mode octal value of posix file mode (000..777) * @return self */ default SELF withFileFromPath(String path, Path filePath, Integer mode) { final MountableFile mountableFile = MountableFile.forHostPath(filePath, mode); return ((SELF) this).withFileFromTransferable(path, mountableFile); } } ================================================ FILE: core/src/main/java/org/testcontainers/images/builder/traits/StringsTrait.java ================================================ package org.testcontainers.images.builder.traits; import lombok.Getter; import org.apache.commons.lang3.StringUtils; import org.testcontainers.images.builder.Transferable; /** * BuildContextBuilder's trait for String-based manipulations. * */ public interface StringsTrait & BuildContextBuilderTrait> { default SELF withFileFromString(String path, String content) { return ((SELF) this).withFileFromTransferable( path, new Transferable() { @Getter byte[] bytes = content.getBytes(); @Override public long getSize() { return bytes.length; } @Override public String getDescription() { return "String: " + StringUtils.abbreviate(content, 100); } } ); } } ================================================ FILE: core/src/main/java/org/testcontainers/jib/JibDockerClient.java ================================================ package org.testcontainers.jib; import com.github.dockerjava.api.command.InspectImageResponse; import com.github.dockerjava.api.command.LoadImageCallback; import com.google.cloud.tools.jib.api.DockerClient; import com.google.cloud.tools.jib.api.ImageDetails; import com.google.cloud.tools.jib.api.ImageReference; import com.google.cloud.tools.jib.http.NotifyingOutputStream; import com.google.cloud.tools.jib.image.ImageTarball; import com.google.common.io.ByteStreams; import lombok.Cleanup; import org.testcontainers.DockerClientFactory; import org.testcontainers.UnstableAPI; import org.testcontainers.images.RemoteDockerImage; import org.testcontainers.utility.DockerImageName; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; import java.util.function.Consumer; @UnstableAPI class JibDockerClient implements DockerClient { private static JibDockerClient instance; private final com.github.dockerjava.api.DockerClient dockerClient = DockerClientFactory.lazyClient(); public static JibDockerClient instance() { if (instance == null) { instance = new JibDockerClient(); } return instance; } @Override public boolean supported(Map map) { return false; } @Override public String load(ImageTarball imageTarball, Consumer writtenByteCountListener) throws IOException { @Cleanup PipedInputStream in = new PipedInputStream(); @Cleanup PipedOutputStream out = new PipedOutputStream(in); LoadImageCallback loadImage = this.dockerClient.loadImageAsyncCmd(in).exec(new LoadImageCallback()); try (NotifyingOutputStream stdin = new NotifyingOutputStream(out, writtenByteCountListener)) { imageTarball.writeTo(stdin); } return loadImage.awaitMessage(); } @Override public void save(ImageReference imageReference, Path outputPath, Consumer writtenByteCountListener) throws IOException { try ( InputStream inputStream = this.dockerClient.saveImageCmd(imageReference.toString()).exec(); InputStream stdout = new BufferedInputStream(inputStream); OutputStream fileStream = new BufferedOutputStream(Files.newOutputStream(outputPath)); NotifyingOutputStream notifyingFileStream = new NotifyingOutputStream(fileStream, writtenByteCountListener) ) { ByteStreams.copy(stdout, notifyingFileStream); } } @Override public ImageDetails inspect(ImageReference imageReference) { new RemoteDockerImage(DockerImageName.parse(imageReference.toString())).get(); InspectImageResponse response = this.dockerClient.inspectImageCmd(imageReference.toString()).exec(); return new JibImageDetails(response.getSize(), response.getId(), response.getRootFS().getLayers()); } } ================================================ FILE: core/src/main/java/org/testcontainers/jib/JibImage.java ================================================ package org.testcontainers.jib; import com.google.cloud.tools.jib.api.Containerizer; import com.google.cloud.tools.jib.api.DockerClient; import com.google.cloud.tools.jib.api.DockerDaemonImage; import com.google.cloud.tools.jib.api.Jib; import com.google.cloud.tools.jib.api.JibContainer; import com.google.cloud.tools.jib.api.JibContainerBuilder; import lombok.SneakyThrows; import org.testcontainers.DockerClientFactory; import org.testcontainers.utility.Base58; import org.testcontainers.utility.LazyFuture; import org.testcontainers.utility.ResourceReaper; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; public class JibImage extends LazyFuture { private final DockerClient dockerClient = JibDockerClient.instance(); private static final Map DEFAULT_LABELS = Stream .of( DockerClientFactory.DEFAULT_LABELS.entrySet().stream(), ResourceReaper.instance().getLabels().entrySet().stream() ) .flatMap(Function.identity()) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); private final String baseImage; private final Function jibContainerBuilderFn; public JibImage(String baseImage, Function jibContainerBuilderFn) { this.baseImage = baseImage; this.jibContainerBuilderFn = jibContainerBuilderFn; } @SneakyThrows @Override protected String resolve() { JibContainerBuilder containerBuilder = Jib.from(this.dockerClient, DockerDaemonImage.named(this.baseImage)); Function applyLabelsFn = jibContainerBuilder -> { for (Map.Entry entry : DEFAULT_LABELS.entrySet()) { jibContainerBuilder.addLabel(entry.getKey(), entry.getValue()); } return jibContainerBuilder; }; JibContainer jibContainer = this.jibContainerBuilderFn.andThen(applyLabelsFn) .apply(containerBuilder) .containerize( Containerizer.to(this.dockerClient, DockerDaemonImage.named(Base58.randomString(8).toLowerCase())) ); return jibContainer.getTargetImage().toString(); } } ================================================ FILE: core/src/main/java/org/testcontainers/jib/JibImageDetails.java ================================================ package org.testcontainers.jib; import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.ImageDetails; import java.security.DigestException; import java.util.ArrayList; import java.util.List; class JibImageDetails implements ImageDetails { private long size; private String imageId; private List layers; public JibImageDetails(long size, String imageId, List layers) { this.size = size; this.imageId = imageId; this.layers = layers; } @Override public long getSize() { return this.size; } @Override public DescriptorDigest getImageId() throws DigestException { return DescriptorDigest.fromDigest(this.imageId); } @Override public List getDiffIds() throws DigestException { List processedDiffIds = new ArrayList<>(this.layers.size()); for (String diffId : this.layers) { processedDiffIds.add(DescriptorDigest.fromDigest(diffId.trim())); } return processedDiffIds; } } ================================================ FILE: core/src/main/java/org/testcontainers/lifecycle/Startable.java ================================================ package org.testcontainers.lifecycle; import java.util.Collections; import java.util.Set; public interface Startable extends AutoCloseable { default Set getDependencies() { return Collections.emptySet(); } void start(); void stop(); @Override default void close() { stop(); } } ================================================ FILE: core/src/main/java/org/testcontainers/lifecycle/Startables.java ================================================ package org.testcontainers.lifecycle; import lombok.experimental.UtilityClass; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Stream; import java.util.stream.StreamSupport; @UtilityClass public class Startables { private static final Executor EXECUTOR = Executors.newCachedThreadPool( new ThreadFactory() { private final AtomicLong COUNTER = new AtomicLong(0); @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r, "testcontainers-lifecycle-" + COUNTER.getAndIncrement()); thread.setDaemon(true); return thread; } } ); /** * @see #deepStart(Stream) */ public CompletableFuture deepStart(Collection startables) { return deepStart((Iterable) startables); } /** * @see #deepStart(Stream) */ public CompletableFuture deepStart(Iterable startables) { return deepStart(StreamSupport.stream(startables.spliterator(), false)); } /** * @see #deepStart(Stream) */ public CompletableFuture deepStart(Startable... startables) { return deepStart(Arrays.stream(startables)); } /** * Start every {@link Startable} recursively and asynchronously and join on the result. * * Performance note: * The method uses and returns {@link CompletableFuture}s to resolve as many {@link Startable}s at once as possible. * This way, for the following graph: * / b \ * a e * c / * d / * "a", "c" and "d" will resolve in parallel, then "b". * * If we would call blocking {@link Startable#start()}, "e" would wait for "b", "b" for "a", and only then "c", and then "d". * But, since "c" and "d" are independent from "a", there is no point in waiting for "a" to be resolved first. * * @param startables a {@link Stream} of {@link Startable}s to start and scan for transitive dependencies. * @return a {@link CompletableFuture} that resolves once all {@link Startable}s have started. */ public CompletableFuture deepStart(Stream startables) { return deepStart(new HashMap<>(), startables); } /** * * @param started an intermediate storage for already started {@link Startable}s to prevent multiple starts. * @param startables a {@link Stream} of {@link Startable}s to start and scan for transitive dependencies. */ private CompletableFuture deepStart( Map> started, Stream startables ) { CompletableFuture[] futures = startables .sequential() .map(it -> { // avoid a recursive update in `computeIfAbsent` Map> subStarted = new HashMap<>(started); CompletableFuture future = started.computeIfAbsent( it, startable -> { return deepStart(subStarted, startable.getDependencies().stream()) .thenRunAsync(startable::start, EXECUTOR); } ); started.putAll(subStarted); return future; }) .toArray(CompletableFuture[]::new); return allOfFailfast(futures); } private static CompletableFuture allOfFailfast(CompletableFuture[] futures) { CompletableFuture result = CompletableFuture.allOf(futures); for (CompletableFuture future : futures) { future.whenComplete((t, ex) -> { if (ex != null) { result.completeExceptionally(ex); } }); } return result; } } ================================================ FILE: core/src/main/java/org/testcontainers/lifecycle/TestDescription.java ================================================ package org.testcontainers.lifecycle; public interface TestDescription { String getTestId(); String getFilesystemFriendlyName(); } ================================================ FILE: core/src/main/java/org/testcontainers/lifecycle/TestLifecycleAware.java ================================================ package org.testcontainers.lifecycle; import java.util.Optional; public interface TestLifecycleAware { default void beforeTest(TestDescription description) {} default void afterTest(TestDescription description, Optional throwable) {} } ================================================ FILE: core/src/main/java/org/testcontainers/utility/AuditLogger.java ================================================ package org.testcontainers.utility; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.dockerjava.api.command.DockerCmd; import com.google.common.base.Strings; import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.MDC; import java.util.List; /** * Logger for tracking potentially destructive actions, intended for usage in a shared Docker environment where * traceability is needed. This class uses SLF4J, logging at TRACE level and capturing common fields as MDC fields. *

* Users should configure their test logging to apply appropriate filters/storage so that these logs are * captured appropriately. */ @Slf4j @UtilityClass public class AuditLogger { private static final ObjectMapper objectMapper = new ObjectMapper(); public static final String MDC_PREFIX = AuditLogger.class.getCanonicalName(); public static void doLog( @NotNull String action, @Nullable String image, @Nullable String containerId, @NotNull DockerCmd cmd ) { doLog(action, image, containerId, cmd, null); } public static void doLog( @NotNull String action, @Nullable String image, @Nullable String containerId, @NotNull DockerCmd cmd, @Nullable Exception e ) { if (!log.isTraceEnabled()) { return; } MDC.put(MDC_PREFIX + ".Action", Strings.nullToEmpty(action)); MDC.put(MDC_PREFIX + ".Image", Strings.nullToEmpty(image)); MDC.put(MDC_PREFIX + ".ContainerId", Strings.nullToEmpty(containerId)); try { MDC.put(MDC_PREFIX + ".Command", objectMapper.writeValueAsString(cmd)); } catch (JsonProcessingException ignored) {} if (e != null) { MDC.put(MDC_PREFIX + ".Exception", e.getLocalizedMessage()); log.trace("{} action with image: {}, containerId: {}", action, image, containerId, e); } else { log.trace("{} action with image: {}, containerId: {}", action, image, containerId); } MDC.remove(MDC_PREFIX + ".Action"); MDC.remove(MDC_PREFIX + ".Image"); MDC.remove(MDC_PREFIX + ".ContainerId"); MDC.remove(MDC_PREFIX + ".Command"); MDC.remove(MDC_PREFIX + ".Exception"); } public static void doComposeLog(@NotNull String[] commandParts, @Nullable List env) { if (!log.isTraceEnabled()) { return; } MDC.put(MDC_PREFIX + ".Action", "COMPOSE"); if (env != null) { MDC.put(MDC_PREFIX + ".Compose.Env", env.toString()); } final String command = StringUtils.join(commandParts, ' '); MDC.put(MDC_PREFIX + ".Compose.Command", command); log.trace("COMPOSE action with command: {}, env: {}", command, env); MDC.remove(MDC_PREFIX + ".Action"); MDC.remove(MDC_PREFIX + ".Compose.Command"); MDC.remove(MDC_PREFIX + ".Compose.Env"); } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/AuthConfigUtil.java ================================================ package org.testcontainers.utility; import com.github.dockerjava.api.model.AuthConfig; import com.google.common.base.MoreObjects; import com.google.common.base.Strings; import lombok.experimental.UtilityClass; import org.jetbrains.annotations.NotNull; /** * TODO: Javadocs */ @UtilityClass public class AuthConfigUtil { public static String toSafeString(AuthConfig authConfig) { if (authConfig == null) { return "null"; } return MoreObjects .toStringHelper(authConfig) .add("username", authConfig.getUsername()) .add("password", obfuscated(authConfig.getPassword())) .add("auth", obfuscated(authConfig.getAuth())) .add("email", authConfig.getEmail()) .add("registryAddress", authConfig.getRegistryAddress()) .add("registryToken", obfuscated(authConfig.getRegistrytoken())) .toString(); } @NotNull private static String obfuscated(String value) { return Strings.isNullOrEmpty(value) ? "blank" : "hidden non-blank value"; } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/Base58.java ================================================ package org.testcontainers.utility; import java.security.SecureRandom; /** * Utility class for creation of random strings of 58 easy-to-distinguish characters. */ public class Base58 { private static final char[] ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray(); private static final SecureRandom RANDOM = new SecureRandom(); public static String randomString(int length) { char[] result = new char[length]; for (int i = 0; i < length; i++) { char pick = ALPHABET[RANDOM.nextInt(ALPHABET.length)]; result[i] = pick; } return new String(result); } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/ClasspathScanner.java ================================================ package org.testcontainers.utility; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.VisibleForTesting; import java.net.URL; import java.util.Collections; import java.util.Comparator; import java.util.Objects; import java.util.stream.Stream; /** * Utility for identifying resource files on classloaders. */ @Slf4j class ClasspathScanner { @VisibleForTesting static Stream scanFor(final String name, ClassLoader... classLoaders) { return Stream .of(classLoaders) .flatMap(classLoader -> getAllPropertyFilesOnClassloader(classLoader, name)) .filter(Objects::nonNull) .sorted( Comparator .comparing(ClasspathScanner::filesFileSchemeFirst) // resolve 'local' files first .thenComparing(URL::toString) // sort alphabetically for the sake of determinism ) .distinct(); } private static Integer filesFileSchemeFirst(final URL t) { return t.getProtocol().equals("file") ? 0 : 1; } /** * @param name the resource name to search for * @return distinct, ordered stream of resources found by searching this class' classloader and the current thread's * context classloader. Results are currently alphabetically sorted. */ static Stream scanFor(final String name) { return scanFor(name, ClasspathScanner.class.getClassLoader(), Thread.currentThread().getContextClassLoader()); } @Nullable private static Stream getAllPropertyFilesOnClassloader(final ClassLoader it, final String s) { try { return Collections.list(it.getResources(s)).stream(); } catch (Exception e) { log.error("Unable to read configuration from classloader {} - this is probably a bug", it, e); return Stream.empty(); } } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/CommandLine.java ================================================ package org.testcontainers.utility; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.zeroturnaround.exec.InvalidExitValueException; import org.zeroturnaround.exec.ProcessExecutor; import org.zeroturnaround.exec.ProcessResult; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.concurrent.TimeoutException; import java.util.regex.Pattern; /** * Process execution utility methods. */ public class CommandLine { private static final Logger LOGGER = LoggerFactory.getLogger(CommandLine.class); /** * Run a shell command synchronously. * * @param command command to run and arguments * @return the stdout output of the command */ public static String runShellCommand(String... command) { String joinedCommand = String.join(" ", command); LOGGER.debug("Executing shell command: `{}`", joinedCommand); try { ProcessResult result = new ProcessExecutor().command(command).readOutput(true).exitValueNormal().execute(); return result.outputUTF8().trim(); } catch (IOException | InterruptedException | TimeoutException | InvalidExitValueException e) { throw new ShellCommandException("Exception when executing " + joinedCommand, e); } } /** * Check whether an executable exists, either at a specific path (if a full path is given) or * on the PATH. * * @param executable the name of an executable on the PATH or a complete path to an executable that may/may not exist * @return whether the executable exists and is executable */ public static boolean executableExists(String executable) { // First check if we've been given the full path already File directFile = new File(executable); if (directFile.exists() && directFile.canExecute()) { return true; } for (String pathString : getSystemPath()) { Path path = Paths.get(pathString); if (Files.exists(path.resolve(executable)) && Files.isExecutable(path.resolve(executable))) { return true; } } return false; } @NotNull public static String[] getSystemPath() { return System.getenv("PATH").split(Pattern.quote(File.pathSeparator)); } private static class ShellCommandException extends RuntimeException { public ShellCommandException(String message, Exception e) { super(message, e); } } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/ComparableVersion.java ================================================ package org.testcontainers.utility; import com.google.common.annotations.VisibleForTesting; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; public final class ComparableVersion implements Comparable { private final int[] parts; public static final ComparableVersion OS_VERSION = new ComparableVersion(System.getProperty("os.version")); public ComparableVersion(String version) { this.parts = parseVersion(version); } @Override public int compareTo(@NotNull ComparableVersion other) { for (int i = 0; i < Math.min(this.parts.length, other.parts.length); i++) { int thisPart = this.parts[i]; int otherPart = other.parts[i]; if (thisPart > otherPart) { return 1; } else if (thisPart < otherPart) { return -1; } } return 0; } public boolean isSemanticVersion() { return parts.length > 0; } public boolean isLessThan(String other) { return this.compareTo(new ComparableVersion(other)) < 0; } public boolean isGreaterThanOrEqualTo(String other) { return this.compareTo(new ComparableVersion(other)) >= 0; } @VisibleForTesting static int[] parseVersion(final String version) { final List parts = new ArrayList<>(5); int acc = 0; for (final char c : version.toCharArray()) { if (c == '.') { parts.add(acc); acc = 0; } if (Character.isDigit(c)) { acc = 10 * acc + Character.digit(c, 10); } } if (acc != 0) { parts.add(acc); } final int[] ret = new int[parts.size()]; for (int i = 0; i < ret.length; i++) { ret[i] = parts.get(i); } return ret; } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/ConfigurationFileImageNameSubstitutor.java ================================================ package org.testcontainers.utility; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; /** * {@link ImageNameSubstitutor} which takes replacement image names from configuration. * See {@link TestcontainersConfiguration} for the subset of image names which can be substituted using this mechanism. */ @Slf4j final class ConfigurationFileImageNameSubstitutor extends ImageNameSubstitutor { private final TestcontainersConfiguration configuration; public ConfigurationFileImageNameSubstitutor() { this(TestcontainersConfiguration.getInstance()); } @VisibleForTesting ConfigurationFileImageNameSubstitutor(TestcontainersConfiguration configuration) { this.configuration = configuration; } @Override public DockerImageName apply(final DockerImageName original) { final DockerImageName result = configuration .getConfiguredSubstituteImage(original) .asCompatibleSubstituteFor(original); if (!result.equals(original)) { log.warn( "Image name {} was substituted by configuration to {}. This approach is deprecated and will be removed in the future", original, result ); } return result; } @Override protected String getDescription() { return getClass().getSimpleName(); } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/DefaultImageNameSubstitutor.java ================================================ package org.testcontainers.utility; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; /** * Testcontainers' default implementation of {@link ImageNameSubstitutor}. * Delegates to {@link ConfigurationFileImageNameSubstitutor} followed by {@link PrefixingImageNameSubstitutor}. */ @Slf4j final class DefaultImageNameSubstitutor extends ImageNameSubstitutor { private final ConfigurationFileImageNameSubstitutor configurationFileImageNameSubstitutor; private final PrefixingImageNameSubstitutor prefixingImageNameSubstitutor; public DefaultImageNameSubstitutor() { configurationFileImageNameSubstitutor = new ConfigurationFileImageNameSubstitutor(); prefixingImageNameSubstitutor = new PrefixingImageNameSubstitutor(); } @VisibleForTesting DefaultImageNameSubstitutor( final ConfigurationFileImageNameSubstitutor configurationFileImageNameSubstitutor, final PrefixingImageNameSubstitutor prefixingImageNameSubstitutor ) { this.configurationFileImageNameSubstitutor = configurationFileImageNameSubstitutor; this.prefixingImageNameSubstitutor = prefixingImageNameSubstitutor; } @Override public DockerImageName apply(final DockerImageName original) { return configurationFileImageNameSubstitutor.andThen(prefixingImageNameSubstitutor).apply(original); } @Override protected String getDescription() { return ( "DefaultImageNameSubstitutor (composite of '" + configurationFileImageNameSubstitutor.getDescription() + "' and '" + prefixingImageNameSubstitutor.getDescription() + "')" ); } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/DockerImageName.java ================================================ package org.testcontainers.utility; import com.google.common.net.HostAndPort; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.With; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.testcontainers.utility.Versioning.Sha256Versioning; import org.testcontainers.utility.Versioning.TagVersioning; import java.util.regex.Pattern; @EqualsAndHashCode(exclude = { "rawName", "compatibleSubstituteFor" }) @AllArgsConstructor(access = AccessLevel.PRIVATE) public final class DockerImageName { /* Regex patterns used for validation */ private static final String ALPHA_NUMERIC = "[a-z0-9]+"; private static final String SEPARATOR = "([.]|_{1,2}|-+)"; private static final String REPO_NAME_PART = ALPHA_NUMERIC + "(" + SEPARATOR + ALPHA_NUMERIC + ")*"; private static final Pattern REPO_NAME = Pattern.compile(REPO_NAME_PART + "(/" + REPO_NAME_PART + ")*"); private static final String LIBRARY_PREFIX = "library/"; private final String rawName; @With @Getter private final String registry; @With @Getter private final String repository; @NotNull @With(AccessLevel.PRIVATE) private final Versioning versioning; @Nullable @With(AccessLevel.PRIVATE) private final DockerImageName compatibleSubstituteFor; /** * Parses a docker image name from a provided string. * * @param fullImageName in standard Docker format, e.g. name:tag, * some.registry/path/name:tag, * some.registry/path/name@sha256:abcdef..., etc. */ public static DockerImageName parse(String fullImageName) { return new DockerImageName(fullImageName); } /** * Parses a docker image name from a provided string. * * @param fullImageName in standard Docker format, e.g. name:tag, * some.registry/path/name:tag, * some.registry/path/name@sha256:abcdef..., etc. * @deprecated use {@link DockerImageName#parse(String)} instead */ @Deprecated public DockerImageName(String fullImageName) { this.rawName = fullImageName; final int slashIndex = fullImageName.indexOf('/'); String remoteName; if ( slashIndex == -1 || ( !fullImageName.substring(0, slashIndex).contains(".") && !fullImageName.substring(0, slashIndex).contains(":") && !fullImageName.substring(0, slashIndex).equals("localhost") ) ) { registry = ""; remoteName = fullImageName; } else { registry = fullImageName.substring(0, slashIndex); remoteName = fullImageName.substring(slashIndex + 1); } if (remoteName.contains("@sha256:")) { repository = remoteName.split("@sha256:")[0]; versioning = new Sha256Versioning(remoteName.split("@sha256:")[1]); } else if (remoteName.contains(":")) { repository = remoteName.split(":")[0]; versioning = new TagVersioning(remoteName.split(":")[1]); } else { repository = remoteName; versioning = Versioning.ANY; } compatibleSubstituteFor = null; } /** * Parses a docker image name from a provided string, and uses a separate provided version. * * @param nameWithoutTag in standard Docker format, e.g. name, * some.registry/path/name, * some.registry/path/name, etc. * @param version a docker image version identifier, either as a tag or sha256 checksum, e.g. * tag, * sha256:abcdef.... * @deprecated use {@link DockerImageName#parse(String)}.{@link DockerImageName#withTag(String)} instead */ @Deprecated public DockerImageName(String nameWithoutTag, @NotNull String version) { this.rawName = nameWithoutTag; final int slashIndex = nameWithoutTag.indexOf('/'); String remoteName; if ( slashIndex == -1 || ( !nameWithoutTag.substring(0, slashIndex).contains(".") && !nameWithoutTag.substring(0, slashIndex).contains(":") && !nameWithoutTag.substring(0, slashIndex).equals("localhost") ) ) { registry = ""; remoteName = nameWithoutTag; } else { registry = nameWithoutTag.substring(0, slashIndex); remoteName = nameWithoutTag.substring(slashIndex + 1); } if (version.startsWith("sha256:")) { repository = remoteName; versioning = new Sha256Versioning(version.replace("sha256:", "")); } else { repository = remoteName; versioning = new TagVersioning(version); } compatibleSubstituteFor = null; } /** * @return the unversioned (non 'tag') part of this name */ public String getUnversionedPart() { if (!"".equals(registry)) { return registry + "/" + repository; } else { return repository; } } /** * @return the versioned part of this name (tag or sha256) */ public String getVersionPart() { return versioning.toString(); } /** * @return canonical name for the image */ public String asCanonicalNameString() { return getUnversionedPart() + versioning.getSeparator() + getVersionPart(); } @Override public String toString() { return asCanonicalNameString(); } /** * Is the image name valid? * * @throws IllegalArgumentException if not valid */ public void assertValid() { //noinspection UnstableApiUsage HostAndPort.fromString(registry); // return value ignored - this throws if registry is not a valid host:port string if (!REPO_NAME.matcher(repository).matches()) { throw new IllegalArgumentException(repository + " is not a valid Docker image name (in " + rawName + ")"); } if (!versioning.isValid()) { throw new IllegalArgumentException( versioning + " is not a valid image versioning identifier (in " + rawName + ")" ); } } /** * @param newTag version tag for the copy to use * @return an immutable copy of this {@link DockerImageName} with the new version tag */ public DockerImageName withTag(final String newTag) { return withVersioning(new TagVersioning(newTag)); } /** * Declare that this {@link DockerImageName} is a compatible substitute for another image - i.e. that this image * behaves as the other does, and is compatible with Testcontainers' assumptions about the other image. * * @param otherImageName the image name of the other image * @return an immutable copy of this {@link DockerImageName} with the compatibility declaration attached. */ public DockerImageName asCompatibleSubstituteFor(String otherImageName) { return withCompatibleSubstituteFor(DockerImageName.parse(otherImageName)); } /** * Declare that this {@link DockerImageName} is a compatible substitute for another image - i.e. that this image * behaves as the other does, and is compatible with Testcontainers' assumptions about the other image. * * @param otherImageName the image name of the other image * @return an immutable copy of this {@link DockerImageName} with the compatibility declaration attached. */ public DockerImageName asCompatibleSubstituteFor(DockerImageName otherImageName) { return withCompatibleSubstituteFor(otherImageName); } /** * Test whether this {@link DockerImageName} has declared compatibility with another image (set using * {@link DockerImageName#asCompatibleSubstituteFor(String)} or * {@link DockerImageName#asCompatibleSubstituteFor(DockerImageName)}. *

* If a version tag part is present in the other image name, the tags must exactly match, unless it * is 'latest'. If a version part is not present in the other image name, the tag contents are ignored. * * @param other the other image that we are trying to test compatibility with * @return whether this image has declared compatibility. */ public boolean isCompatibleWith(DockerImageName other) { // Make sure we always compare against a version of the image name containing the LIBRARY_PREFIX String finalImageName; if (this.repository.startsWith(LIBRARY_PREFIX)) { finalImageName = this.repository; } else { finalImageName = LIBRARY_PREFIX + this.repository; } DockerImageName imageWithLibraryPrefix = DockerImageName.parse(finalImageName); if (other.equals(this) || imageWithLibraryPrefix.equals(this)) { return true; } if (this.compatibleSubstituteFor == null) { return false; } return this.compatibleSubstituteFor.isCompatibleWith(other); } /** * Behaves as {@link DockerImageName#isCompatibleWith(DockerImageName)} but throws an exception * rather than returning false if a mismatch is detected. * * @param anyOthers the other image(s) that we are trying to check compatibility with. If more * than one is provided, this method will check compatibility with at least one * of them. * @throws IllegalStateException if {@link DockerImageName#isCompatibleWith(DockerImageName)} * returns false */ public void assertCompatibleWith(DockerImageName... anyOthers) { if (anyOthers.length == 0) { throw new IllegalArgumentException("anyOthers parameter must be non-empty"); } for (DockerImageName anyOther : anyOthers) { if (this.isCompatibleWith(anyOther)) { return; } } final DockerImageName exampleOther = anyOthers[0]; throw new IllegalStateException( String.format( "Failed to verify that image '%s' is a compatible substitute for '%s'. This generally means that " + "you are trying to use an image that Testcontainers has not been designed to use. If this is " + "deliberate, and if you are confident that the image is compatible, you should declare " + "compatibility in code using the `asCompatibleSubstituteFor` method. For example:\n" + " DockerImageName myImage = DockerImageName.parse(\"%s\").asCompatibleSubstituteFor(\"%s\");\n" + "and then use `myImage` instead.", this.rawName, exampleOther.rawName, this.rawName, exampleOther.rawName ) ); } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/DockerLoggerFactory.java ================================================ package org.testcontainers.utility; import lombok.AccessLevel; import lombok.NoArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class DockerLoggerFactory { public static Logger getLogger(String dockerImageName) { final String abbreviatedName; if (dockerImageName.contains("@sha256")) { abbreviatedName = dockerImageName.substring(0, dockerImageName.indexOf("@sha256") + 14) + "..."; } else { abbreviatedName = dockerImageName; } return LoggerFactory.getLogger("tc." + abbreviatedName); } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/DockerMachineClient.java ================================================ package org.testcontainers.utility; import lombok.NonNull; import org.apache.commons.lang3.SystemUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Arrays; import java.util.List; import java.util.Optional; /** * Created by rnorth on 27/10/2015. */ public class DockerMachineClient { private static DockerMachineClient instance; private static final Logger LOGGER = LoggerFactory.getLogger(DockerMachineClient.class); private static final String executableName; static { if (SystemUtils.IS_OS_WINDOWS) { executableName = "docker-machine.exe"; } else { executableName = "docker-machine"; } } /** * Private constructor */ private DockerMachineClient() {} /** * Obtain an instance of the DockerMachineClient wrapper. * * @return the singleton instance of DockerMachineClient */ public static synchronized DockerMachineClient instance() { if (instance == null) { instance = new DockerMachineClient(); } return instance; } public boolean isInstalled() { return CommandLine.executableExists(executableName); } public Optional getDefaultMachine() { String ls = CommandLine.runShellCommand(executableName, "ls", "-q"); List machineNames = Arrays.asList(ls.split("\n")); String envMachineName = System.getenv("DOCKER_MACHINE_NAME"); if (machineNames.contains(envMachineName)) { LOGGER.debug("Using docker-machine set in DOCKER_MACHINE_NAME: {}", envMachineName); return Optional.of(envMachineName); } else if (machineNames.contains("default")) { LOGGER.debug("DOCKER_MACHINE_NAME is not set; Using 'default' docker-machine", envMachineName); return Optional.of("default"); } else if (machineNames.size() > 0) { LOGGER.debug( "DOCKER_MACHINE_NAME is not set and no machine named 'default' found; Using first machine found with `docker-machine ls`: {}", machineNames.get(0) ); return Optional.of(machineNames.get(0)); } else { return Optional.empty(); } } public void ensureMachineRunning(@NonNull String machineName) { if (!isMachineRunning(machineName)) { LOGGER.info("Docker-machine '{}' is not running. Will start it now", machineName); CommandLine.runShellCommand("docker-machine", "start", machineName); } } /** * @deprecated Use getDockerDaemonUrl(@NonNull String machineName) for connection to docker-machine */ @Deprecated public String getDockerDaemonIpAddress(@NonNull String machineName) { return CommandLine.runShellCommand(executableName, "ip", machineName); } public String getDockerDaemonUrl(@NonNull String machineName) { return CommandLine.runShellCommand(executableName, "url", machineName); } public boolean isMachineRunning(String machineName) { String status = CommandLine.runShellCommand("docker-machine", "status", machineName); return status.trim().equalsIgnoreCase("running"); } public boolean isDefaultMachineRunning() { return isMachineRunning(getDefaultMachine().orElse("default")); } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/DockerStatus.java ================================================ package org.testcontainers.utility; import com.github.dockerjava.api.command.InspectContainerResponse; import java.time.Duration; import java.time.Instant; import java.time.format.DateTimeFormatter; /** * Utility functions for dealing with docker status based on the information available to us, and trying to be * defensive. *

*

In docker-java version 2.2.0, which we're using, only these * fields are available in the container state returned from Docker Inspect: "isRunning", "isPaused", "startedAt", and * "finishedAt". There are states that can occur (including "created", "OOMkilled" and "dead") that aren't directly * shown through this result. *

*

Docker also doesn't seem to use null values for timestamps; see DOCKER_TIMESTAMP_ZERO, below. */ public class DockerStatus { /** * When the docker client has an "empty" timestamp, it returns this special value, rather than * null or an empty string. */ static final String DOCKER_TIMESTAMP_ZERO = "0001-01-01T00:00:00Z"; /** * Based on this status, is this container running, and has it been doing so for the specified amount of time? * * @param state the state provided by InspectContainer * @param minimumRunningDuration minimum duration to consider this as "solidly" running, or null * @param now the time to consider as the current time * @return true if we can conclude that the container is running, false otherwise */ public static boolean isContainerRunning( InspectContainerResponse.ContainerState state, Duration minimumRunningDuration, Instant now ) { if (state.getRunning()) { if (minimumRunningDuration == null) { return true; } Instant startedAt = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(state.getStartedAt(), Instant::from); if (startedAt.isBefore(now.minus(minimumRunningDuration))) { return true; } } return false; } /** * Based on this status, has the container halted? * * @param state the state provided by InspectContainer * @return true if we can conclude that the container has started but is now stopped, false otherwise. */ public static boolean isContainerStopped(InspectContainerResponse.ContainerState state) { // get some preconditions out of the way if (state.getRunning() || state.getPaused()) { return false; } // if the finished timestamp is non-empty, that means the container started and finished. boolean hasStarted = isDockerTimestampNonEmpty(state.getStartedAt()); boolean hasFinished = isDockerTimestampNonEmpty(state.getFinishedAt()); return hasStarted && hasFinished; } public static boolean isDockerTimestampNonEmpty(String dockerTimestamp) { // This is a defensive approach. Current versions of Docker use the DOCKER_TIMESTAMP_ZERO value, but // that could change. return ( dockerTimestamp != null && !dockerTimestamp.isEmpty() && !dockerTimestamp.equals(DOCKER_TIMESTAMP_ZERO) && DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(dockerTimestamp, Instant::from).getEpochSecond() >= 0L ); } public static boolean isContainerExitCodeSuccess(InspectContainerResponse.ContainerState state) { int exitCode = state.getExitCode(); // 0 is the only exit code we can consider as success return exitCode == 0; } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/DynamicPollInterval.java ================================================ package org.testcontainers.utility; import org.awaitility.pollinterval.PollInterval; import java.time.Duration; import java.time.Instant; /** * Awaitility {@link org.awaitility.pollinterval.PollInterval} that takes execution time into consideration, * to allow a constant poll-interval, as opposed to Awaitility's default poll-delay behaviour. * * @deprecated For internal usage only. */ @Deprecated public class DynamicPollInterval implements PollInterval { final Duration interval; Instant lastTimestamp; private DynamicPollInterval(Duration interval) { this.interval = interval; lastTimestamp = Instant.now(); } public static DynamicPollInterval of(Duration duration) { return new DynamicPollInterval(duration); } public static DynamicPollInterval ofMillis(long millis) { return DynamicPollInterval.of(Duration.ofMillis(millis)); } @Override public Duration next(int pollCount, Duration previousDuration) { Instant now = Instant.now(); Duration executionDuration = Duration.between(lastTimestamp, now); Duration result = interval.minusMillis(Math.min(interval.toMillis(), executionDuration.toMillis())); lastTimestamp = now.plus(result); return result; } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/ImageNameSubstitutor.java ================================================ package org.testcontainers.utility; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; import org.testcontainers.UnstableAPI; import java.util.ServiceLoader; import java.util.function.Function; import java.util.stream.StreamSupport; /** * An image name substitutor converts a Docker image name, as may be specified in code, to an alternative name. * This is intended to provide a way to override image names, for example to enforce pulling of images from a private * registry. *

* This is marked as @{@link UnstableAPI} as this API is new. While we do not think major changes will be required, we * will react to feedback if necessary. */ @Slf4j @UnstableAPI public abstract class ImageNameSubstitutor implements Function { @VisibleForTesting static ImageNameSubstitutor instance; @VisibleForTesting static ImageNameSubstitutor defaultImplementation = new DefaultImageNameSubstitutor(); public static synchronized ImageNameSubstitutor instance() { return instance(Thread.currentThread().getContextClassLoader()); } @VisibleForTesting static synchronized ImageNameSubstitutor instance(ClassLoader classLoader) { if (instance == null) { ImageNameSubstitutor configuredInstance = getImageNameSubstitutor(classLoader); if (configuredInstance != null) { log.debug( "Attempting to instantiate an ImageNameSubstitutor with class: {}", configuredInstance.getClass().getCanonicalName() ); log.info("Found configured ImageNameSubstitutor: {}", configuredInstance.getDescription()); instance = new ChainedImageNameSubstitutor( wrapWithLogging(defaultImplementation), wrapWithLogging(configuredInstance) ); } else { instance = wrapWithLogging(defaultImplementation); } log.info("Image name substitution will be performed by: {}", instance.getDescription()); } return instance; } private static ImageNameSubstitutor getImageNameSubstitutor(ClassLoader classLoader) { final String configuredClassName = TestcontainersConfiguration.getInstance().getImageSubstitutorClassName(); if (configuredClassName != null) { try { return (ImageNameSubstitutor) classLoader.loadClass(configuredClassName).getConstructor().newInstance(); } catch (Exception e) { throw new IllegalArgumentException( "Configured Image Substitutor could not be loaded: " + configuredClassName, e ); } } return StreamSupport .stream(ServiceLoader.load(ImageNameSubstitutor.class, classLoader).spliterator(), false) .findFirst() .orElse(null); } public static ImageNameSubstitutor noop() { return new NoopImageNameSubstitutor(); } private static ImageNameSubstitutor wrapWithLogging(final ImageNameSubstitutor wrappedInstance) { return new LogWrappedImageNameSubstitutor(wrappedInstance); } /** * Substitute a {@link DockerImageName} for another, for example to replace a generic Docker Hub image name with a * private registry copy of the image. * * @param original original name to be replaced * @return a replacement name, or the original, as appropriate */ public abstract DockerImageName apply(DockerImageName original); /** * @return a human-readable description of the substitutor */ protected abstract String getDescription(); /** * Wrapper substitutor which logs which substitutions have been performed. */ static class LogWrappedImageNameSubstitutor extends ImageNameSubstitutor { @VisibleForTesting final ImageNameSubstitutor wrappedInstance; public LogWrappedImageNameSubstitutor(final ImageNameSubstitutor wrappedInstance) { this.wrappedInstance = wrappedInstance; } @Override public DockerImageName apply(final DockerImageName original) { final DockerImageName replacementImage = wrappedInstance.apply(original); if (!replacementImage.equals(original)) { log.info( "Using {} as a substitute image for {} (using image substitutor: {})", replacementImage.asCanonicalNameString(), original.asCanonicalNameString(), wrappedInstance.getDescription() ); return replacementImage; } else { log.debug( "Did not find a substitute image for {} (using image substitutor: {})", original.asCanonicalNameString(), wrappedInstance.getDescription() ); return original; } } @Override protected String getDescription() { return wrappedInstance.getDescription(); } } /** * Wrapper substitutor that passes the original image name through a default substitutor and then the configured one */ static class ChainedImageNameSubstitutor extends ImageNameSubstitutor { private final ImageNameSubstitutor defaultInstance; private final ImageNameSubstitutor configuredInstance; public ChainedImageNameSubstitutor( ImageNameSubstitutor defaultInstance, ImageNameSubstitutor configuredInstance ) { this.defaultInstance = defaultInstance; this.configuredInstance = configuredInstance; } @Override public DockerImageName apply(DockerImageName original) { return defaultInstance.andThen(configuredInstance).apply(original); } @Override protected String getDescription() { return String.format( "Chained substitutor of '%s' and then '%s'", defaultInstance.getDescription(), configuredInstance.getDescription() ); } @Override public String toString() { return getDescription(); } } private static class NoopImageNameSubstitutor extends ImageNameSubstitutor { @Override public DockerImageName apply(DockerImageName original) { return original; } @Override protected String getDescription() { return "No-op substitutor"; } } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/JVMHookResourceReaper.java ================================================ package org.testcontainers.utility; import com.github.dockerjava.api.model.Container; import com.github.dockerjava.api.model.PruneType; import java.util.Arrays; import java.util.List; import java.util.Map; /** * A {@link ResourceReaper} implementation that uses {@link Runtime#addShutdownHook(Thread)} * to cleanup containers. */ class JVMHookResourceReaper extends ResourceReaper { @Override public void init() { setHook(); } @Override public synchronized void performCleanup() { super.performCleanup(); synchronized (DEATH_NOTE) { DEATH_NOTE.forEach(filters -> prune(PruneType.CONTAINERS, filters)); DEATH_NOTE.forEach(filters -> prune(PruneType.NETWORKS, filters)); DEATH_NOTE.forEach(filters -> prune(PruneType.VOLUMES, filters)); DEATH_NOTE.forEach(filters -> prune(PruneType.IMAGES, filters)); } } private void prune(PruneType pruneType, List> filters) { String[] labels = filters .stream() .filter(it -> "label".equals(it.getKey())) .map(Map.Entry::getValue) .toArray(String[]::new); switch (pruneType) { // Docker only prunes stopped containers, so we have to do it manually case CONTAINERS: List containers = dockerClient .listContainersCmd() .withFilter("label", Arrays.asList(labels)) .withShowAll(true) .exec(); containers .parallelStream() .forEach(container -> { dockerClient .removeContainerCmd(container.getId()) .withForce(true) .withRemoveVolumes(true) .exec(); }); break; default: dockerClient.pruneCmd(pruneType).withLabelFilter(labels).exec(); break; } } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/LazyFuture.java ================================================ package org.testcontainers.utility; import lombok.AccessLevel; import lombok.Getter; import org.rnorth.ducttape.timeouts.Timeouts; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; /** * Future implementation with lazy result evaluation in the same Thread as caller. * * @param */ public abstract class LazyFuture implements Future { @Getter(value = AccessLevel.MODULE, lazy = true) private final T resolvedValue = resolve(); protected abstract T resolve(); @Override public boolean cancel(boolean mayInterruptIfRunning) { return false; } @Override public boolean isCancelled() { return false; } @Override public boolean isDone() { return ((AtomicReference) resolvedValue).get() != null; } @Override public T get() { return getResolvedValue(); } @Override public T get(long timeout, TimeUnit unit) throws TimeoutException { try { return Timeouts.getWithTimeout((int) timeout, unit, this::get); } catch (org.rnorth.ducttape.TimeoutException e) { throw new TimeoutException(e.getMessage()); } } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/LicenseAcceptance.java ================================================ package org.testcontainers.utility; import com.google.common.base.Charsets; import com.google.common.io.Resources; import lombok.experimental.UtilityClass; import java.net.URL; import java.util.List; /** * Utility class to ensure that licenses have been accepted by the developer. */ @UtilityClass public class LicenseAcceptance { private static final String ACCEPTANCE_FILE_NAME = "container-license-acceptance.txt"; public static void assertLicenseAccepted(final String imageName) { try { final URL url = Resources.getResource(ACCEPTANCE_FILE_NAME); final List acceptedLicences = Resources.readLines(url, Charsets.UTF_8); if (acceptedLicences.stream().map(String::trim).anyMatch(imageName::equals)) { return; } } catch (Exception ignored) { // suppressed } throw new IllegalStateException( "The image " + imageName + " requires you to accept a license agreement. " + "Please place a file at the root of the classpath named " + ACCEPTANCE_FILE_NAME + ", e.g. at " + "src/test/resources/" + ACCEPTANCE_FILE_NAME + ". This file should contain the line:\n " + imageName ); } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/LogUtils.java ================================================ package org.testcontainers.utility; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.LogContainerCmd; import lombok.SneakyThrows; import lombok.experimental.UtilityClass; import org.testcontainers.containers.output.FrameConsumerResultCallback; import org.testcontainers.containers.output.OutputFrame; import org.testcontainers.containers.output.ToStringConsumer; import org.testcontainers.containers.output.WaitingConsumer; import java.io.Closeable; import java.io.IOException; import java.util.function.Consumer; /** * Provides utility methods for logging. */ @UtilityClass public class LogUtils { /** * Attach a log consumer to a container's log outputs in follow mode. The consumer will receive all previous * and all future log frames of the specified type(s). * * @param dockerClient a Docker client * @param containerId container ID to attach to * @param consumer a consumer of {@link OutputFrame}s * @param types types of {@link OutputFrame} to receive */ public void followOutput( DockerClient dockerClient, String containerId, Consumer consumer, OutputFrame.OutputType... types ) { attachConsumer(dockerClient, containerId, consumer, true, types); } /** * Attach a log consumer to a container's log outputs in follow mode. The consumer will receive all previous * and all future log frames (both stdout and stderr). * * @param dockerClient a Docker client * @param containerId container ID to attach to * @param consumer a consumer of {@link OutputFrame}s */ public void followOutput(DockerClient dockerClient, String containerId, Consumer consumer) { followOutput(dockerClient, containerId, consumer, OutputFrame.OutputType.STDOUT, OutputFrame.OutputType.STDERR); } /** * Retrieve all previous log outputs for a container of the specified type(s). * * @param dockerClient a Docker client * @param containerId container ID to attach to * @param types types of {@link OutputFrame} to receive * @return all previous output frames (stdout/stderr being separated by newline characters) */ @SneakyThrows(IOException.class) public String getOutput(DockerClient dockerClient, String containerId, OutputFrame.OutputType... types) { if (containerId == null) { return ""; } if (types.length == 0) { types = new OutputFrame.OutputType[] { OutputFrame.OutputType.STDOUT, OutputFrame.OutputType.STDERR }; } final ToStringConsumer consumer = new ToStringConsumer(); final WaitingConsumer wait = new WaitingConsumer(); try (Closeable closeable = attachConsumer(dockerClient, containerId, consumer.andThen(wait), false, types)) { wait.waitUntilEnd(); return consumer.toUtf8String(); } } private static Closeable attachConsumer( DockerClient dockerClient, String containerId, Consumer consumer, boolean followStream, OutputFrame.OutputType... types ) { final LogContainerCmd cmd = dockerClient .logContainerCmd(containerId) .withFollowStream(followStream) .withSince(0); final FrameConsumerResultCallback callback = new FrameConsumerResultCallback(); for (OutputFrame.OutputType type : types) { callback.addConsumer(type, consumer); if (type == OutputFrame.OutputType.STDOUT) { cmd.withStdOut(true); } if (type == OutputFrame.OutputType.STDERR) { cmd.withStdErr(true); } } return cmd.exec(callback); } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/MountableFile.java ================================================ package org.testcontainers.utility; import com.google.common.base.Charsets; import lombok.AccessLevel; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.apache.commons.compress.archivers.tar.TarConstants; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.SystemUtils; import org.jetbrains.annotations.NotNull; import org.testcontainers.DockerClientFactory; import org.testcontainers.UnstableAPI; import org.testcontainers.images.builder.Transferable; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLDecoder; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Enumeration; import java.util.HashSet; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.stream.Stream; import java.util.zip.Checksum; /** * An abstraction over files and classpath resources aimed at encapsulating all the complexity of generating * a path that the Docker daemon is about to create a volume mount for. */ @RequiredArgsConstructor(access = AccessLevel.PACKAGE) @Slf4j public class MountableFile implements Transferable { private static final String TESTCONTAINERS_TMP_DIR_PREFIX = ".testcontainers-tmp-"; private static final String OS_MAC_TMP_DIR = "/tmp"; private static final int BASE_FILE_MODE = 0100000; private static final int BASE_DIR_MODE = 0040000; private final String path; private final Integer forcedFileMode; @Getter(lazy = true) private final String resolvedPath = resolvePath(); @Getter(lazy = true) private final String filesystemPath = resolveFilesystemPath(); private String resourcePath; /** * Obtains a {@link MountableFile} corresponding to a resource on the classpath (including resources in JAR files) * * @param resourceName the classpath path to the resource * @return a {@link MountableFile} that may be used to obtain a mountable path */ public static MountableFile forClasspathResource(@NotNull final String resourceName) { return forClasspathResource(resourceName, null); } /** * Obtains a {@link MountableFile} corresponding to a file on the docker host filesystem. * * @param path the path to the resource * @return a {@link MountableFile} that may be used to obtain a mountable path */ public static MountableFile forHostPath(@NotNull final String path) { return forHostPath(path, null); } /** * Obtains a {@link MountableFile} corresponding to a file on the docker host filesystem. * * @param path the path to the resource * @return a {@link MountableFile} that may be used to obtain a mountable path */ public static MountableFile forHostPath(final Path path) { return forHostPath(path, null); } /** * Obtains a {@link MountableFile} corresponding to a resource on the classpath (including resources in JAR files) * * @param resourceName the classpath path to the resource * @param mode octal value of posix file mode (000..777) * @return a {@link MountableFile} that may be used to obtain a mountable path */ public static MountableFile forClasspathResource(@NotNull final String resourceName, Integer mode) { return new MountableFile(getClasspathResource(resourceName, new HashSet<>()).toString(), mode); } /** * Obtains a {@link MountableFile} corresponding to a file on the docker host filesystem. * * @param path the path to the resource * @param mode octal value of posix file mode (000..777) * @return a {@link MountableFile} that may be used to obtain a mountable path */ public static MountableFile forHostPath(@NotNull final String path, Integer mode) { return forHostPath(Paths.get(path), mode); } /** * Obtains a {@link MountableFile} corresponding to a file on the docker host filesystem. * * @param path the path to the resource * @param mode octal value of posix file mode (000..777) * @return a {@link MountableFile} that may be used to obtain a mountable path */ public static MountableFile forHostPath(final Path path, Integer mode) { return new MountableFile(path.toAbsolutePath().toString(), mode); } @NotNull private static URL getClasspathResource( @NotNull final String resourcePath, @NotNull final Set classLoaders ) { final Set classLoadersToSearch = new HashSet<>(classLoaders); // try context and system classloaders as well classLoadersToSearch.add(Thread.currentThread().getContextClassLoader()); classLoadersToSearch.add(ClassLoader.getSystemClassLoader()); classLoadersToSearch.add(MountableFile.class.getClassLoader()); for (final ClassLoader classLoader : classLoadersToSearch) { if (classLoader == null) { continue; } URL resource = classLoader.getResource(resourcePath); if (resource != null) { return resource; } // Be lenient if an absolute path was given if (resourcePath.startsWith("/")) { resource = classLoader.getResource(resourcePath.replaceFirst("/", "")); if (resource != null) { return resource; } } } throw new IllegalArgumentException( "Resource with path " + resourcePath + " could not be found on any of these classloaders: " + classLoadersToSearch ); } private static String unencodeResourceURIToFilePath(@NotNull final String resource) { try { // Convert any url-encoded characters (e.g. spaces) back into unencoded form return URLDecoder .decode(resource.replaceAll("\\+", "%2B"), Charsets.UTF_8.name()) .replaceFirst("jar:", "") .replaceFirst("file:", "") .replaceAll("!.*", ""); } catch (UnsupportedEncodingException e) { throw new IllegalStateException(e); } } /** * Obtain a path that the Docker daemon should be able to use to volume mount a file/resource * into a container. If this is a classpath resource residing in a JAR, it will be extracted to * a temporary location so that the Docker daemon is able to access it. * * @return a volume-mountable path. */ private String resolvePath() { String result = getResourcePath(); // Special case for Windows if (SystemUtils.IS_OS_WINDOWS && result.startsWith("/")) { // Remove leading / result = result.substring(1); } return result; } /** * Obtain a path in local filesystem that the Docker daemon should be able to use to volume mount a file/resource * into a container. If this is a classpath resource residing in a JAR, it will be extracted to * a temporary location so that the Docker daemon is able to access it. * * TODO: rename method accordingly and check if really needed like this * * @return */ private String resolveFilesystemPath() { String result = getResourcePath(); if (SystemUtils.IS_OS_WINDOWS && result.startsWith("/")) { result = PathUtils.createMinGWPath(result).substring(1); } return result; } private String getResourcePath() { if (path.contains(".jar!")) { resourcePath = extractClassPathResourceToTempLocation(this.path); } else { resourcePath = unencodeResourceURIToFilePath(path); } return resourcePath; } /** * Extract a file or directory tree from a JAR file to a temporary location. * This allows Docker to mount classpath resources as files. * * @param hostPath the path on the host, expected to be of the format 'file:/path/to/some.jar!/classpath/path/to/resource' * @return the path of the temporary file/directory */ private String extractClassPathResourceToTempLocation(final String hostPath) { File tmpLocation = createTempDirectory(); //noinspection ResultOfMethodCallIgnored tmpLocation.delete(); String urldecodedJarPath = unencodeResourceURIToFilePath(hostPath); String internalPath = hostPath.replaceAll("[^!]*!/", ""); try (JarFile jarFile = new JarFile(urldecodedJarPath)) { Enumeration entries = jarFile.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); final String name = entry.getName(); if (name.startsWith(internalPath)) { log.debug( "Copying classpath resource(s) from {} to {} to permit Docker to bind", hostPath, tmpLocation ); copyFromJarToLocation(jarFile, entry, internalPath, tmpLocation); } } } catch (IOException e) { throw new IllegalStateException( "Failed to process JAR file when extracting classpath resource: " + hostPath, e ); } // Mark temporary files/dirs for deletion at JVM shutdown deleteOnExit(tmpLocation.toPath()); try { return tmpLocation.getCanonicalPath(); } catch (IOException e) { throw new IllegalStateException(e); } } private File createTempDirectory() { try { if (SystemUtils.IS_OS_MAC) { return Files.createTempDirectory(Paths.get(OS_MAC_TMP_DIR), TESTCONTAINERS_TMP_DIR_PREFIX).toFile(); } return Files.createTempDirectory(TESTCONTAINERS_TMP_DIR_PREFIX).toFile(); } catch (IOException e) { return new File(TESTCONTAINERS_TMP_DIR_PREFIX + Base58.randomString(5)); } } @SuppressWarnings("ResultOfMethodCallIgnored") private void copyFromJarToLocation( final JarFile jarFile, final JarEntry entry, final String fromRoot, final File toRoot ) throws IOException { String destinationName = entry.getName().replaceFirst(fromRoot, ""); File newFile = new File(toRoot, destinationName); log.debug("Copying resource {} from JAR file {}", fromRoot, jarFile.getName()); if (!entry.isDirectory()) { // Create parent directories Path parent = newFile.getAbsoluteFile().toPath().getParent(); parent.toFile().mkdirs(); newFile.deleteOnExit(); try (InputStream is = jarFile.getInputStream(entry)) { Files.copy(is, newFile.toPath()); } catch (IOException e) { log.error( "Failed to extract classpath resource " + entry.getName() + " from JAR file " + jarFile.getName(), e ); throw e; } } } private void deleteOnExit(final Path path) { Runtime .getRuntime() .addShutdownHook( new Thread(DockerClientFactory.TESTCONTAINERS_THREAD_GROUP, () -> PathUtils.recursiveDeleteDir(path)) ); } /** * {@inheritDoc} */ @Override public void transferTo(final TarArchiveOutputStream outputStream, String destinationPathInTar) { recursiveTar(destinationPathInTar, this.getResolvedPath(), this.getResolvedPath(), outputStream); } /* * Recursively copies a file/directory into a TarArchiveOutputStream */ private void recursiveTar( String entryFilename, String rootPath, String itemPath, TarArchiveOutputStream tarArchive ) { try { final File sourceFile = new File(itemPath).getCanonicalFile(); // e.g. /foo/bar/baz final File sourceRootFile = new File(rootPath).getCanonicalFile(); // e.g. /foo final String relativePathToSourceFile = sourceRootFile .toPath() .relativize(sourceFile.toPath()) .toFile() .toString(); // e.g. /bar/baz final String tarEntryFilename; if (relativePathToSourceFile.isEmpty()) { tarEntryFilename = entryFilename; // entry filename e.g. xyz => xyz } else { tarEntryFilename = entryFilename + "/" + relativePathToSourceFile; // entry filename e.g. /xyz/bar/baz => /foo/bar/baz } final TarArchiveEntry tarEntry = new TarArchiveEntry(sourceFile, tarEntryFilename.replaceAll("^/", "")); // TarArchiveEntry automatically sets the mode for file/directory, but we can update to ensure that the mode is set exactly (inc executable bits) tarEntry.setMode(getUnixFileMode(itemPath)); tarArchive.putArchiveEntry(tarEntry); if (sourceFile.isFile()) { Files.copy(sourceFile.toPath(), tarArchive); } // a directory entry merely needs to exist in the TAR file - there is no data stored yet tarArchive.closeArchiveEntry(); final File[] children = sourceFile.listFiles(); if (children != null) { // recurse into child files/directories for (final File child : children) { recursiveTar( entryFilename, sourceRootFile.getCanonicalPath(), child.getCanonicalPath(), tarArchive ); } } } catch (IOException e) { log.error("Error when copying TAR file entry: {}", itemPath, e); throw new UncheckedIOException(e); // fail fast } } @Override public long getSize() { final File file = new File(this.getResolvedPath()); if (file.isFile()) { return file.length(); } else { return 0; } } @Override public String getDescription() { return this.getResolvedPath(); } @Override public void updateChecksum(Checksum checksum) { File file = new File(getResolvedPath()); checksumFile(file, checksum); } @SneakyThrows(IOException.class) private void checksumFile(File file, Checksum checksum) { Path path = file.toPath(); checksum.update(MountableFile.getUnixFileMode(path)); if (file.isDirectory()) { try (Stream stream = Files.walk(path)) { stream .filter(it -> it != path) .forEach(it -> { checksumFile(it.toFile(), checksum); }); } } else { FileUtils.checksum(file, checksum); } } @Override public int getFileMode() { return getUnixFileMode(this.getResolvedPath()); } private int getUnixFileMode(final String pathAsString) { final Path path = Paths.get(pathAsString); if (this.forcedFileMode != null) { return this.getModeValue(path); } return getUnixFileMode(path); } @UnstableAPI public static int getUnixFileMode(final Path path) { try { int unixMode = (int) Files.readAttributes(path, "unix:mode").get("mode"); // Truncate mode bits for z/OS if ( "OS/390".equals(SystemUtils.OS_NAME) || "z/OS".equals(SystemUtils.OS_NAME) || "zOS".equals(SystemUtils.OS_NAME) ) { unixMode &= TarConstants.MAXID; unixMode |= Files.isDirectory(path) ? 040000 : 0100000; } return unixMode; } catch (IOException | UnsupportedOperationException e) { // fallback for non-posix environments int mode = DEFAULT_FILE_MODE; if (Files.isDirectory(path)) { mode = DEFAULT_DIR_MODE; } else if (Files.isExecutable(path)) { mode |= 0111; // equiv to +x for user/group/others } return mode; } } private int getModeValue(final Path path) { int result = Files.isDirectory(path) ? BASE_DIR_MODE : BASE_FILE_MODE; return result | this.forcedFileMode; } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/PathUtils.java ================================================ package org.testcontainers.utility; import lombok.NonNull; import lombok.experimental.UtilityClass; import java.io.IOException; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; /** * Filesystem operation utility methods. */ @UtilityClass public class PathUtils { /** * Recursively delete a directory and all its subdirectories and files. * * @param directory path to the directory to delete. */ public static void recursiveDeleteDir(final @NonNull Path directory) { try { Files.walkFileTree( directory, new SimpleFileVisitor() { @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { Files.delete(dir); return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.delete(file); return FileVisitResult.CONTINUE; } } ); } catch (IOException ignored) {} } /** * Make a directory, plus any required parent directories. * * @param directory the directory path to make */ public static void mkdirp(Path directory) { boolean result = directory.toFile().mkdirs(); if (!result) { throw new IllegalStateException("Failed to create directory at: " + directory); } } /** * Create a MinGW compatible path based on usual Windows path * * @param path a usual windows path * @return a MinGW compatible path */ public static String createMinGWPath(String path) { String mingwPath = path.replace('\\', '/'); int driveLetterIndex = 1; if (mingwPath.matches("^[a-zA-Z]:\\/.*")) { driveLetterIndex = 0; } // drive-letter must be lower case mingwPath = "//" + Character.toLowerCase(mingwPath.charAt(driveLetterIndex)) + mingwPath.substring(driveLetterIndex + 1); mingwPath = mingwPath.replace(":", ""); return mingwPath; } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/PrefixingImageNameSubstitutor.java ================================================ package org.testcontainers.utility; import com.google.common.annotations.VisibleForTesting; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; /** * An {@link ImageNameSubstitutor} which applies a prefix to all image names, e.g. a private registry host and path. * The prefix may be set via an environment variable (TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX) or an equivalent * configuration file entry (see {@link TestcontainersConfiguration}). */ @NoArgsConstructor @Slf4j final class PrefixingImageNameSubstitutor extends ImageNameSubstitutor { @VisibleForTesting static final String PREFIX_PROPERTY_KEY = "hub.image.name.prefix"; private TestcontainersConfiguration configuration = TestcontainersConfiguration.getInstance(); @VisibleForTesting PrefixingImageNameSubstitutor(final TestcontainersConfiguration configuration) { this.configuration = configuration; } @Override public DockerImageName apply(DockerImageName original) { final String configuredPrefix = configuration.getEnvVarOrProperty(PREFIX_PROPERTY_KEY, ""); if (configuredPrefix.isEmpty()) { log.debug("No prefix is configured"); return original; } boolean isAHubImage = original.getRegistry().isEmpty(); if (!isAHubImage) { log.debug("Image {} is not a Docker Hub image - not applying registry/repository change", original); return original; } log.debug("Applying changes to image name {}: applying prefix '{}'", original, configuredPrefix); DockerImageName prefixAsImage = DockerImageName.parse(configuredPrefix); return original .withRegistry(prefixAsImage.getRegistry()) .withRepository(prefixAsImage.getRepository() + original.getRepository()); } @Override protected String getDescription() { return getClass().getSimpleName(); } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/RegistryAuthLocator.java ================================================ package org.testcontainers.utility; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.dockerjava.api.exception.NotFoundException; import com.github.dockerjava.api.model.AuthConfig; import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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; import java.io.File; import java.io.IOException; import java.util.Base64; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; /** * Utility to look up registry authentication information for an image. */ public class RegistryAuthLocator { private static final Logger log = LoggerFactory.getLogger(RegistryAuthLocator.class); private static final String DEFAULT_REGISTRY_NAME = "https://index.docker.io/v1/"; private static final String DOCKER_AUTH_ENV_VAR = "DOCKER_AUTH_CONFIG"; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private static RegistryAuthLocator instance; private final String commandPathPrefix; private final String commandExtension; private final File configFile; private final String configEnv; private final Map> cache = new ConcurrentHashMap<>(); /** * key - credential helper's name * value - helper's response for "credentials not found" use case */ private final Map CREDENTIALS_HELPERS_NOT_FOUND_MESSAGE_CACHE; @VisibleForTesting RegistryAuthLocator( File configFile, String configEnv, String commandPathPrefix, String commandExtension, Map notFoundMessageHolderReference ) { this.configFile = configFile; this.configEnv = configEnv; this.commandPathPrefix = commandPathPrefix; this.commandExtension = commandExtension; this.CREDENTIALS_HELPERS_NOT_FOUND_MESSAGE_CACHE = notFoundMessageHolderReference; } /** */ protected RegistryAuthLocator() { final String dockerConfigLocation = System .getenv() .getOrDefault("DOCKER_CONFIG", System.getProperty("user.home") + "/.docker"); this.configFile = new File(dockerConfigLocation + "/config.json"); this.configEnv = System.getenv(DOCKER_AUTH_ENV_VAR); this.commandPathPrefix = ""; this.commandExtension = ""; this.CREDENTIALS_HELPERS_NOT_FOUND_MESSAGE_CACHE = new HashMap<>(); } public static synchronized RegistryAuthLocator instance() { if (instance == null) { instance = new RegistryAuthLocator(); } return instance; } @VisibleForTesting static void setInstance(RegistryAuthLocator overrideInstance) { instance = overrideInstance; } /** * Looks up an AuthConfig for a given image name. *

* Lookup is performed in following order, as per * https://docs.docker.com/engine/reference/commandline/cli/: *

    *
  1. {@code credHelpers}
  2. *
  3. {@code credsStore}
  4. *
  5. Hard-coded Base64 encoded auth in {@code auths}
  6. *
  7. otherwise, if no credentials have been found then behaviour falls back to docker-java's * implementation
  8. *
* * @param dockerImageName image name to be looked up (potentially including a registry URL part) * @param defaultAuthConfig an AuthConfig object that should be returned if there is no overriding authentication available for images that are looked up * @return an AuthConfig that is applicable to this specific image OR the defaultAuthConfig. */ public AuthConfig lookupAuthConfig(DockerImageName dockerImageName, AuthConfig defaultAuthConfig) { final String registryName = effectiveRegistryName(dockerImageName); log.debug("Looking up auth config for image: {} at registry: {}", dockerImageName, registryName); final Optional cachedAuth = cache.computeIfAbsent( registryName, __ -> lookupUncachedAuthConfig(registryName, dockerImageName) ); if (cachedAuth.isPresent()) { log.debug("Cached auth found: [{}]", AuthConfigUtil.toSafeString(cachedAuth.get())); return cachedAuth.get(); } else { log.debug( "No matching Auth Configs - falling back to defaultAuthConfig [{}]", AuthConfigUtil.toSafeString(defaultAuthConfig) ); // otherwise, defaultAuthConfig should already contain any credentials available return defaultAuthConfig; } } private Optional lookupUncachedAuthConfig(String registryName, DockerImageName dockerImageName) { try { final JsonNode config = getDockerAuthConfig(); log.debug("registryName [{}] for dockerImageName [{}]", registryName, dockerImageName); // use helper preferentially (per https://docs.docker.com/engine/reference/commandline/cli/) final AuthConfig helperAuthConfig = authConfigUsingHelper(config, registryName); if (helperAuthConfig != null) { log.debug("found helper auth config [{}]", AuthConfigUtil.toSafeString(helperAuthConfig)); return Optional.of(helperAuthConfig); } // no credsHelper to use, using credsStore: final AuthConfig storeAuthConfig = authConfigUsingStore(config, registryName); if (storeAuthConfig != null) { log.debug("found creds store auth config [{}]", AuthConfigUtil.toSafeString(storeAuthConfig)); return Optional.of(storeAuthConfig); } // fall back to base64 encoded auth hardcoded in config file final AuthConfig existingAuthConfig = findExistingAuthConfig(config, registryName); if (existingAuthConfig != null) { log.debug("found existing auth config [{}]", AuthConfigUtil.toSafeString(existingAuthConfig)); return Optional.of(existingAuthConfig); } } catch (Exception e) { log.info( "Failure when attempting to lookup auth config. Please ignore if you don't have images in an authenticated registry. Details: (dockerImageName: {}, configFile: {}, configEnv: {}). Falling back to docker-java default behaviour. Exception message: {}", dockerImageName, configFile, DOCKER_AUTH_ENV_VAR, e.getMessage() ); } return Optional.empty(); } private JsonNode getDockerAuthConfig() throws Exception { log.debug( "RegistryAuthLocator has configFile: {} ({}) configEnv: {} ({}) and commandPathPrefix: {}", configFile, configFile.exists() ? "exists" : "does not exist", DOCKER_AUTH_ENV_VAR, configEnv != null ? "exists" : "does not exist", commandPathPrefix ); if (configEnv != null) { log.debug("RegistryAuthLocator reading from environment variable: {}", DOCKER_AUTH_ENV_VAR); return OBJECT_MAPPER.readTree(configEnv); } else if (configFile.exists()) { log.debug("RegistryAuthLocator reading from configFile: {}", configFile); return OBJECT_MAPPER.readTree(configFile); } throw new NotFoundException( "No config supplied. Checked in order: " + configFile + " (file not found), " + DOCKER_AUTH_ENV_VAR + " (not set)" ); } private AuthConfig findExistingAuthConfig(final JsonNode config, final String reposName) throws Exception { final Map.Entry entry = findAuthNode(config, reposName); if (entry != null && entry.getValue() != null && entry.getValue().size() > 0) { final AuthConfig deserializedAuth = OBJECT_MAPPER .treeToValue(entry.getValue(), AuthConfig.class) .withRegistryAddress(entry.getKey()); if ( StringUtils.isBlank(deserializedAuth.getUsername()) && StringUtils.isBlank(deserializedAuth.getPassword()) && !StringUtils.isBlank(deserializedAuth.getAuth()) ) { final String rawAuth = new String(Base64.getDecoder().decode(deserializedAuth.getAuth())); final String[] splitRawAuth = rawAuth.split(":", 2); if (splitRawAuth.length == 2) { deserializedAuth.withUsername(splitRawAuth[0]); deserializedAuth.withPassword(splitRawAuth[1]); } } return deserializedAuth; } return null; } private AuthConfig authConfigUsingHelper(final JsonNode config, final String reposName) throws Exception { final JsonNode credHelpers = config.get("credHelpers"); if (credHelpers != null && credHelpers.size() > 0) { final JsonNode helperNode = credHelpers.get(reposName); if (helperNode != null && helperNode.isTextual()) { final String helper = helperNode.asText(); return runCredentialProvider(reposName, helper); } } return null; } private AuthConfig authConfigUsingStore(final JsonNode config, final String reposName) throws Exception { final JsonNode credsStoreNode = config.get("credsStore"); if (credsStoreNode != null && !credsStoreNode.isMissingNode() && credsStoreNode.isTextual()) { final String credsStore = credsStoreNode.asText(); if (StringUtils.isBlank(credsStore)) { log.warn("Docker auth config credsStore field will be ignored, because value is blank"); return null; } return runCredentialProvider(reposName, credsStore); } return null; } private Map.Entry findAuthNode(final JsonNode config, final String reposName) { final JsonNode auths = config.get("auths"); if (auths != null && auths.size() > 0) { final Iterator> fields = auths.fields(); while (fields.hasNext()) { final Map.Entry entry = fields.next(); if (entry.getKey().endsWith("://" + reposName) || entry.getKey().equals(reposName)) { return entry; } } } return null; } private AuthConfig runCredentialProvider(String hostName, String helperOrStoreName) throws Exception { if (StringUtils.isBlank(hostName)) { log.debug("There is no point in locating AuthConfig for blank hostName. Returning NULL to allow fallback"); return null; } final String credentialProgramName = getCredentialProgramName(helperOrStoreName); final CredentialOutput data; log.debug( "Executing docker credential provider: {} to locate auth config for: {}", credentialProgramName, hostName ); try { data = runCredentialProgram(hostName, credentialProgramName); if (data.getExitValue() == 1) { final String responseErrorMsg = data.getStdout(); if (!StringUtils.isBlank(responseErrorMsg)) { String credentialsNotFoundMsg = getGenericCredentialsNotFoundMsg( responseErrorMsg, credentialProgramName ); if (credentialsNotFoundMsg != null && credentialsNotFoundMsg.equals(responseErrorMsg)) { log.info( "Credential helper/store ({}) does not have credentials for {}", credentialProgramName, hostName ); return null; } log.debug( "Failure running docker credential helper/store ({}) with output '{}' and error '{}'", credentialProgramName, responseErrorMsg, data.getStderr() ); } else { log.debug("Failure running docker credential helper/store ({})", credentialProgramName); } throw new InvalidResultException(data.getStdout(), null); } } catch (Exception e) { log.debug("Failure running docker credential helper/store ({})", credentialProgramName); throw e; } final JsonNode helperResponse = OBJECT_MAPPER.readTree(data.getStdout()); log.debug("Credential helper/store provided auth config for: {}", hostName); final String username = helperResponse.at("/Username").asText(); final String password = helperResponse.at("/Secret").asText(); final String serverUrl = helperResponse.at("/ServerURL").asText(); final AuthConfig authConfig = new AuthConfig().withRegistryAddress(serverUrl.isEmpty() ? hostName : serverUrl); if ("".equals(username)) { return authConfig.withIdentityToken(password); } else { return authConfig.withUsername(username).withPassword(password); } } private String getCredentialProgramName(String credHelper) { return commandPathPrefix + "docker-credential-" + credHelper + commandExtension; } private String effectiveRegistryName(DockerImageName dockerImageName) { final String registry = dockerImageName.getRegistry(); if (!StringUtils.isEmpty(registry)) { return registry; } return StringUtils.defaultString( DockerClientFactory.instance().getInfo().getIndexServerAddress(), DEFAULT_REGISTRY_NAME ); } private String getGenericCredentialsNotFoundMsg(String credentialsNotFoundMsg, String credentialHelperName) { if (!CREDENTIALS_HELPERS_NOT_FOUND_MESSAGE_CACHE.containsKey(credentialHelperName)) { CREDENTIALS_HELPERS_NOT_FOUND_MESSAGE_CACHE.put(credentialHelperName, credentialsNotFoundMsg); } return CREDENTIALS_HELPERS_NOT_FOUND_MESSAGE_CACHE.get(credentialHelperName); } private CredentialOutput runCredentialProgram(String hostName, String credentialHelperName) throws InterruptedException, TimeoutException, IOException { String[] command = SystemUtils.IS_OS_WINDOWS ? new String[] { "cmd", "/c", credentialHelperName, "get" } : new String[] { credentialHelperName, "get" }; StringBuffer stdout = new StringBuffer(); StringBuffer stderr = new StringBuffer(); 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()); } } ) .timeout(30, TimeUnit.SECONDS) .execute(); int exitValue = processResult.getExitValue(); 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(int exitValue, String stdout, String stderr) { this.exitValue = exitValue; this.stdout = stdout.trim(); this.stderr = stderr.trim(); } int getExitValue() { return this.exitValue; } String getStdout() { return this.stdout; } String getStderr() { return this.stderr; } } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/ResourceReaper.java ================================================ package org.testcontainers.utility; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.CreateContainerCmd; import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.api.exception.NotFoundException; import com.github.dockerjava.api.model.Network; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Throwables; import com.google.common.collect.Sets; import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.GenericContainer; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import java.util.stream.Stream; /** * Component that responsible for container removal and automatic cleanup of dead containers at JVM shutdown. */ @Slf4j public class ResourceReaper { private static final Logger LOGGER = LoggerFactory.getLogger(ResourceReaper.class); private static final Map MARKER_LABELS = Collections.singletonMap( DockerClientFactory.TESTCONTAINERS_SESSION_ID_LABEL, DockerClientFactory.SESSION_ID ); static final List>> DEATH_NOTE = new ArrayList<>( Arrays.asList( Stream .concat(DockerClientFactory.DEFAULT_LABELS.entrySet().stream(), MARKER_LABELS.entrySet().stream()) .>map(it -> new SimpleEntry<>("label", it.getKey() + "=" + it.getValue())) .collect(Collectors.toList()) ) ); private static ResourceReaper instance; final DockerClient dockerClient = DockerClientFactory.lazyClient(); private Map registeredContainers = new ConcurrentHashMap<>(); private Set registeredNetworks = Sets.newConcurrentHashSet(); private Set registeredImages = Sets.newConcurrentHashSet(); private AtomicBoolean hookIsSet = new AtomicBoolean(false); /** * Internal constructor to avoid custom implementations */ ResourceReaper() {} public static synchronized ResourceReaper instance() { if (instance == null) { boolean useRyuk = !Boolean.parseBoolean(System.getenv("TESTCONTAINERS_RYUK_DISABLED")); if (useRyuk) { //noinspection deprecation instance = new RyukResourceReaper(); } else { String ryukDisabledMessage = "\n" + "********************************************************************************" + "\n" + "Ryuk has been disabled. This can cause unexpected behavior in your environment." + "\n" + "********************************************************************************"; LOGGER.warn(ryukDisabledMessage); instance = new JVMHookResourceReaper(); } } return instance; } /** * Perform a cleanup. * @deprecated no longer supported API, use {@link DockerClient} directly */ @Deprecated public void performCleanup() { registeredContainers.forEach(this::removeContainer); registeredNetworks.forEach(this::removeNetwork); registeredImages.forEach(this::removeImage); } /** * Register a filter to be cleaned up. * * @param filter the filter * @deprecated only label filter is supported by the prune API, use {@link #registerLabelsFilterForCleanup(Map)} */ @Deprecated public void registerFilterForCleanup(List> filter) { synchronized (DEATH_NOTE) { DEATH_NOTE.add(filter); DEATH_NOTE.notifyAll(); } } /** * Register a label to be cleaned up. * * @param labels the filter */ public void registerLabelsFilterForCleanup(Map labels) { registerFilterForCleanup( labels .entrySet() .stream() .map(it -> new SimpleEntry<>("label", it.getKey() + "=" + it.getValue())) .collect(Collectors.toList()) ); } /** * Register a container to be cleaned up, either on explicit call to stopAndRemoveContainer, or at JVM shutdown. * * @param containerId the ID of the container * @param imageName the image name of the container (used for logging) * @deprecated no longer supported API */ @Deprecated public void registerContainerForCleanup(String containerId, String imageName) { setHook(); registeredContainers.put(containerId, imageName); } /** * Stop a potentially running container and remove it, including associated volumes. * * @param containerId the ID of the container * @deprecated use {@link DockerClient} directly */ @Deprecated public void stopAndRemoveContainer(String containerId) { removeContainer(containerId, registeredContainers.get(containerId)); registeredContainers.remove(containerId); } /** * Stop a potentially running container and remove it, including associated volumes. * * @param containerId the ID of the container * @param imageName the image name of the container (used for logging) * @deprecated use {@link DockerClient} directly */ @Deprecated public void stopAndRemoveContainer(String containerId, String imageName) { removeContainer(containerId, imageName); registeredContainers.remove(containerId); } private void removeContainer(String containerId, String imageName) { boolean running; try { InspectContainerResponse containerInfo = dockerClient.inspectContainerCmd(containerId).exec(); running = containerInfo.getState() != null && Boolean.TRUE.equals(containerInfo.getState().getRunning()); } catch (NotFoundException e) { LOGGER.trace("Was going to stop container but it apparently no longer exists: {}", containerId); return; } catch (Exception e) { LOGGER.trace( "Error encountered when checking container for shutdown (ID: {}) - it may not have been stopped, or may already be stopped. Root cause: {}", containerId, Throwables.getRootCause(e).getMessage() ); return; } if (running) { try { LOGGER.trace("Stopping container: {}", containerId); dockerClient.killContainerCmd(containerId).exec(); LOGGER.trace("Stopped container: {}", imageName); } catch (Exception e) { LOGGER.trace( "Error encountered shutting down container (ID: {}) - it may not have been stopped, or may already be stopped. Root cause: {}", containerId, Throwables.getRootCause(e).getMessage() ); } } try { dockerClient.inspectContainerCmd(containerId).exec(); } catch (Exception e) { LOGGER.trace("Was going to remove container but it apparently no longer exists: {}", containerId); return; } try { LOGGER.trace("Removing container: {}", containerId); dockerClient.removeContainerCmd(containerId).withRemoveVolumes(true).withForce(true).exec(); LOGGER.debug("Removed container and associated volume(s): {}", imageName); } catch (Exception e) { LOGGER.trace( "Error encountered shutting down container (ID: {}) - it may not have been stopped, or may already be stopped. Root cause: {}", containerId, Throwables.getRootCause(e).getMessage() ); } } /** * Register a network to be cleaned up at JVM shutdown. * * @param id the ID of the network * @deprecated no longer supported API */ @Deprecated public void registerNetworkIdForCleanup(String id) { setHook(); registeredNetworks.add(id); } /** * Removes a network by ID. * @param id * @deprecated use {@link DockerClient} directly */ @Deprecated public void removeNetworkById(String id) { removeNetwork(id); } private void removeNetwork(String id) { try { List networks; try { // Try to find the network if it still exists // Listing by ID first prevents docker-java logging an error if we just go blindly into removeNetworkCmd networks = dockerClient.listNetworksCmd().withIdFilter(id).exec(); } catch (Exception e) { LOGGER.trace( "Error encountered when looking up network for removal (name: {}) - it may not have been removed", id ); return; } // at this point networks should contain either 0 or 1 entries, depending on whether the network exists // using a for loop we essentially treat the network like an optional, only applying the removal if it exists for (Network network : networks) { try { dockerClient.removeNetworkCmd(network.getId()).exec(); registeredNetworks.remove(network.getId()); LOGGER.debug("Removed network: {}", id); } catch (Exception e) { LOGGER.trace( "Error encountered removing network (name: {}) - it may not have been removed", network.getName() ); } } } finally { registeredNetworks.remove(id); } } /** * @deprecated no longer supported API */ @Deprecated public void unregisterNetwork(String identifier) { registeredNetworks.remove(identifier); } /** * @deprecated no longer supported API */ @Deprecated public void unregisterContainer(String identifier) { registeredContainers.remove(identifier); } /** * @deprecated no longer supported API */ @Deprecated public void registerImageForCleanup(String dockerImageName) { setHook(); registeredImages.add(dockerImageName); } private void removeImage(String dockerImageName) { LOGGER.trace("Removing image tagged {}", dockerImageName); try { dockerClient.removeImageCmd(dockerImageName).withForce(true).exec(); } catch (Throwable e) { LOGGER.warn("Unable to delete image " + dockerImageName, e); } } void setHook() { if (hookIsSet.compareAndSet(false, true)) { // If the JVM stops without containers being stopped, try and stop the container. Runtime .getRuntime() .addShutdownHook(new Thread(DockerClientFactory.TESTCONTAINERS_THREAD_GROUP, this::performCleanup)); } } /** * * @deprecated internal API */ @Deprecated public Map getLabels() { return MARKER_LABELS; } /** * * @deprecated internal API */ @Deprecated public CreateContainerCmd register(GenericContainer container, CreateContainerCmd cmd) { cmd.getLabels().putAll(getLabels()); return cmd; } /** * @deprecated internal API */ @Deprecated public void init() {} static class FilterRegistry { @VisibleForTesting static final String ACKNOWLEDGMENT = "ACK"; private final BufferedReader in; private final OutputStream out; FilterRegistry(InputStream ryukInputStream, OutputStream ryukOutputStream) { this.in = new BufferedReader(new InputStreamReader(ryukInputStream)); this.out = ryukOutputStream; } /** * Registers the given filters with Ryuk * * @param filters the filter to register * @return true if the filters have been registered successfully, false otherwise * @throws IOException if communication with Ryuk fails */ protected boolean register(List> filters) throws IOException { String query = filters .stream() .map(it -> { try { return ( URLEncoder.encode(it.getKey(), "UTF-8") + "=" + URLEncoder.encode(it.getValue(), "UTF-8") ); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } }) .collect(Collectors.joining("&")); log.debug("Sending '{}' to Ryuk", query); out.write(query.getBytes()); out.write('\n'); out.flush(); return waitForAcknowledgment(in); } private static boolean waitForAcknowledgment(BufferedReader in) throws IOException { String line = in.readLine(); while (line != null && !ACKNOWLEDGMENT.equalsIgnoreCase(line)) { line = in.readLine(); } return ACKNOWLEDGMENT.equalsIgnoreCase(line); } } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/RyukContainer.java ================================================ package org.testcontainers.utility; import com.github.dockerjava.api.model.Bind; import com.github.dockerjava.api.model.Volume; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; class RyukContainer extends GenericContainer { RyukContainer() { super("testcontainers/ryuk:0.14.0"); withExposedPorts(8080); withCreateContainerCmdModifier(cmd -> { cmd.withName("testcontainers-ryuk-" + DockerClientFactory.SESSION_ID); cmd.withHostConfig( cmd .getHostConfig() .withAutoRemove(true) .withPrivileged(TestcontainersConfiguration.getInstance().isRyukPrivileged()) .withBinds( new Bind( DockerClientFactory.instance().getRemoteDockerUnixSocketPath(), new Volume("/var/run/docker.sock") ) ) ); }); waitingFor(Wait.forLogMessage(".*Started.*", 1)); } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/RyukResourceReaper.java ================================================ package org.testcontainers.utility; import com.github.dockerjava.api.command.CreateContainerCmd; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.rnorth.ducttape.ratelimits.RateLimiter; import org.rnorth.ducttape.ratelimits.RateLimiterBuilder; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.GenericContainer; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; /** * Ryuk-based {@link ResourceReaper} implementation. * * @see moby-ryuk */ @Slf4j class RyukResourceReaper extends ResourceReaper { private static final RateLimiter RYUK_ACK_RATE_LIMITER = RateLimiterBuilder .newBuilder() .withRate(4, TimeUnit.SECONDS) .withConstantThroughput() .build(); private final AtomicBoolean started = new AtomicBoolean(false); private final RyukContainer ryukContainer = new RyukContainer(); @Override public void init() { if (!TestcontainersConfiguration.getInstance().environmentSupportsReuse()) { log.debug("Ryuk is enabled"); maybeStart(); log.info("Ryuk started - will monitor and terminate Testcontainers containers on JVM exit"); } else { log.debug("Ryuk is enabled but will be started on demand"); } } @Override public void registerLabelsFilterForCleanup(Map labels) { maybeStart(); super.registerLabelsFilterForCleanup(labels); } @Override public Map getLabels() { maybeStart(); return super.getLabels(); } @Override public CreateContainerCmd register(GenericContainer container, CreateContainerCmd cmd) { if (container == ryukContainer) { // Do not register Ryuk container to avoid self-pruning return cmd; } maybeStart(); return super.register(container, cmd); } @SneakyThrows(InterruptedException.class) private synchronized void maybeStart() { if (!started.compareAndSet(false, true)) { return; } ryukContainer.start(); CountDownLatch ryukScheduledLatch = new CountDownLatch(1); String host = ryukContainer.getHost(); Integer ryukPort = ryukContainer.getFirstMappedPort(); Thread kiraThread = new Thread( DockerClientFactory.TESTCONTAINERS_THREAD_GROUP, () -> { while (true) { RYUK_ACK_RATE_LIMITER.doWhenReady(() -> { int index = 0; // not set the read timeout, as Ryuk would not send anything unless a new filter is submitted, meaning that we would get a timeout exception pretty quick try (Socket clientSocket = new Socket()) { clientSocket.connect(new InetSocketAddress(host, ryukPort), 5 * 1000); ResourceReaper.FilterRegistry registry = new ResourceReaper.FilterRegistry( clientSocket.getInputStream(), clientSocket.getOutputStream() ); synchronized (ResourceReaper.DEATH_NOTE) { while (true) { if (ResourceReaper.DEATH_NOTE.size() <= index) { try { ResourceReaper.DEATH_NOTE.wait(1_000); continue; } catch (InterruptedException e) { throw new RuntimeException(e); } } List> filters = ResourceReaper.DEATH_NOTE.get(index); boolean isAcknowledged = registry.register(filters); if (isAcknowledged) { log.debug("Received 'ACK' from Ryuk"); ryukScheduledLatch.countDown(); index++; } else { log.debug("Didn't receive 'ACK' from Ryuk. Will retry to send filters."); } } } } catch (IOException e) { log.warn("Can not connect to Ryuk at {}:{}", host, ryukPort, e); } }); } }, "testcontainers-ryuk" ); kiraThread.setDaemon(true); kiraThread.start(); // We need to wait before we can start any containers to make sure that we delete them if (!ryukScheduledLatch.await(TestcontainersConfiguration.getInstance().getRyukTimeout(), TimeUnit.SECONDS)) { log.error("Timed out waiting for Ryuk container to start. Ryuk's logs:\n{}", ryukContainer.getLogs()); throw new IllegalStateException(String.format("Could not connect to Ryuk at %s:%s", host, ryukPort)); } } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/TestEnvironment.java ================================================ package org.testcontainers.utility; import org.testcontainers.DockerClientFactory; import org.testcontainers.dockerclient.DockerMachineClientProviderStrategy; /** * Provides utility methods for determining facts about the test environment. */ public class TestEnvironment { private TestEnvironment() {} public static boolean dockerApiAtLeast(String minimumVersion) { ComparableVersion min = new ComparableVersion(minimumVersion); ComparableVersion current = new ComparableVersion(DockerClientFactory.instance().getActiveApiVersion()); return current.compareTo(min) >= 0; } public static boolean dockerExecutionDriverSupportsExec() { String executionDriver = DockerClientFactory.instance().getActiveExecutionDriver(); // Could be null starting from Docker 1.13 return executionDriver == null || !executionDriver.startsWith("lxc"); } public static boolean dockerIsDockerMachine() { return DockerClientFactory.instance().isUsing(DockerMachineClientProviderStrategy.class); } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java ================================================ package org.testcontainers.utility; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; import lombok.Data; import lombok.Getter; import lombok.NonNull; import lombok.SneakyThrows; import lombok.Synchronized; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.testcontainers.UnstableAPI; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.Map; import java.util.Optional; import java.util.Properties; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; /** * Provides a mechanism for fetching configuration/default settings. *

* Configuration may be provided in: *

    *
  • A file in the user's home directory named .testcontainers.properties
  • *
  • A file in the classpath named testcontainers.properties
  • *
  • Environment variables
  • *
*

* Note that, if using environment variables, property names are in upper case separated by underscores, preceded by * TESTCONTAINERS_. */ @Data @Slf4j public class TestcontainersConfiguration { private static String PROPERTIES_FILE_NAME = "testcontainers.properties"; private static File USER_CONFIG_FILE = new File(System.getProperty("user.home"), "." + PROPERTIES_FILE_NAME); private static final String AMBASSADOR_IMAGE = "richnorth/ambassador"; private static final String SOCAT_IMAGE = "alpine/socat"; private static final String VNC_RECORDER_IMAGE = "testcontainers/vnc-recorder"; private static final String COMPOSE_IMAGE = "docker/compose"; private static final String ALPINE_IMAGE = "alpine"; private static final String RYUK_IMAGE = "testcontainers/ryuk"; private static final String KAFKA_IMAGE = "confluentinc/cp-kafka"; private static final String PULSAR_IMAGE = "apachepulsar/pulsar"; private static final String LOCALSTACK_IMAGE = "localstack/localstack"; private static final String SSHD_IMAGE = "testcontainers/sshd"; private static final String ORACLE_IMAGE = "gvenzl/oracle-xe"; private static final ImmutableMap CONTAINER_MAPPING = ImmutableMap .builder() .put(DockerImageName.parse(AMBASSADOR_IMAGE), "ambassador.container.image") .put(DockerImageName.parse(SOCAT_IMAGE), "socat.container.image") .put(DockerImageName.parse(VNC_RECORDER_IMAGE), "vncrecorder.container.image") .put(DockerImageName.parse(COMPOSE_IMAGE), "compose.container.image") .put(DockerImageName.parse(ALPINE_IMAGE), "tinyimage.container.image") .put(DockerImageName.parse(RYUK_IMAGE), "ryuk.container.image") .put(DockerImageName.parse(KAFKA_IMAGE), "kafka.container.image") .put(DockerImageName.parse(PULSAR_IMAGE), "pulsar.container.image") .put(DockerImageName.parse(LOCALSTACK_IMAGE), "localstack.container.image") .put(DockerImageName.parse(SSHD_IMAGE), "sshd.container.image") .put(DockerImageName.parse(ORACLE_IMAGE), "oracle.container.image") .build(); @Getter(lazy = true) private static final TestcontainersConfiguration instance = loadConfiguration(); @SuppressWarnings({ "ConstantConditions", "unchecked", "rawtypes" }) @VisibleForTesting static AtomicReference getInstanceField() { // Lazy Getter from Lombok changes the field's type to AtomicReference return (AtomicReference) (Object) instance; } private final Properties userProperties; private final Properties classpathProperties; private final Map environment; TestcontainersConfiguration( Properties userProperties, Properties classpathProperties, final Map environment ) { this.userProperties = userProperties; this.classpathProperties = classpathProperties; this.environment = environment; } @Deprecated public String getAmbassadorContainerImage() { return getImage(AMBASSADOR_IMAGE).asCanonicalNameString(); } @Deprecated public String getSocatContainerImage() { return getImage(SOCAT_IMAGE).asCanonicalNameString(); } @Deprecated public String getVncRecordedContainerImage() { return getImage(VNC_RECORDER_IMAGE).asCanonicalNameString(); } @Deprecated public String getDockerComposeContainerImage() { return getImage(COMPOSE_IMAGE).asCanonicalNameString(); } @Deprecated public String getTinyImage() { return getImage(ALPINE_IMAGE).asCanonicalNameString(); } public boolean isRyukPrivileged() { return Boolean.parseBoolean(getEnvVarOrProperty("ryuk.container.privileged", "true")); } @Deprecated public String getRyukImage() { return getImage(RYUK_IMAGE).asCanonicalNameString(); } @Deprecated public String getSSHdImage() { return getImage(SSHD_IMAGE).asCanonicalNameString(); } public Integer getRyukTimeout() { return Integer.parseInt(getEnvVarOrProperty("ryuk.container.timeout", "30")); } @Deprecated public String getKafkaImage() { return getImage(KAFKA_IMAGE).asCanonicalNameString(); } @Deprecated public String getOracleImage() { return getImage(ORACLE_IMAGE).asCanonicalNameString(); } @Deprecated public String getPulsarImage() { return getImage(PULSAR_IMAGE).asCanonicalNameString(); } @Deprecated public String getLocalStackImage() { return getImage(LOCALSTACK_IMAGE).asCanonicalNameString(); } public boolean isDisableChecks() { return Boolean.parseBoolean(getEnvVarOrUserProperty("checks.disable", "false")); } @UnstableAPI public boolean environmentSupportsReuse() { return Boolean.parseBoolean(getEnvVarOrUserProperty("testcontainers.reuse.enable", "false")); } public String getDockerClientStrategyClassName() { // getConfigurable won't apply the TESTCONTAINERS_ prefix when looking for env vars if DOCKER_ appears at the beginning. // Because of this overlap, and the desire to not change this specific TESTCONTAINERS_DOCKER_CLIENT_STRATEGY setting, // we special-case the logic here so that docker.client.strategy is used when reading properties files and // TESTCONTAINERS_DOCKER_CLIENT_STRATEGY is used when searching environment variables. // looks for TESTCONTAINERS_ prefixed env var only String prefixedEnvVarStrategy = environment.get("TESTCONTAINERS_DOCKER_CLIENT_STRATEGY"); if (prefixedEnvVarStrategy != null) { return prefixedEnvVarStrategy; } // looks for unprefixed env var or unprefixed property, or null if the strategy is not set at all return getEnvVarOrUserProperty("docker.client.strategy", null); } public String getTransportType() { return getEnvVarOrProperty("transport.type", "httpclient5"); } public Integer getImagePullPauseTimeout() { return Integer.parseInt(getEnvVarOrProperty("pull.pause.timeout", "30")); } public Integer getImagePullTimeout() { return Integer.parseInt(getEnvVarOrProperty("pull.timeout", "120")); } public String getImageSubstitutorClassName() { return getEnvVarOrProperty("image.substitutor", null); } public String getImagePullPolicy() { return getEnvVarOrProperty("pull.policy", null); } public Integer getClientPingTimeout() { return Integer.parseInt(getEnvVarOrProperty("client.ping.timeout", "10")); } @Nullable @Contract("_, !null, _ -> !null") private String getConfigurable( @NotNull final String propertyName, @Nullable final String defaultValue, Properties... propertiesSources ) { String envVarName = propertyName.replaceAll("\\.", "_").toUpperCase(); if (!envVarName.startsWith("TESTCONTAINERS_") && !envVarName.startsWith("DOCKER_")) { envVarName = "TESTCONTAINERS_" + envVarName; } if (environment.containsKey(envVarName)) { String value = environment.get(envVarName); if (!value.isEmpty()) { return value; } } for (final Properties properties : propertiesSources) { if (properties.get(propertyName) != null) { return (String) properties.get(propertyName); } } return defaultValue; } /** * Gets a configured setting from an environment variable (if present) or a configuration file property otherwise. * The configuration file will be the .testcontainers.properties file in the user's home directory or * a testcontainers.properties found on the classpath. *

* Note that when searching environment variables, the prefix `TESTCONTAINERS_` will usually be applied to the * property name, which will be converted to upper-case with underscore separators. This prefix will not be added * if the property name begins `docker.`. * * @param propertyName name of configuration file property (dot-separated lower case) * @return the found value, or null if not set */ @Contract("_, !null -> !null") public String getEnvVarOrProperty(@NotNull final String propertyName, @Nullable final String defaultValue) { return getConfigurable(propertyName, defaultValue, userProperties, classpathProperties); } /** * Gets a configured setting from an environment variable (if present) or a configuration file property otherwise. * The configuration file will be the .testcontainers.properties file in the user's home directory. *

* Note that when searching environment variables, the prefix `TESTCONTAINERS_` will usually be applied to the * property name, which will be converted to upper-case with underscore separators. This prefix will not be added * if the property name begins `docker.`. * * @param propertyName name of configuration file property (dot-separated lower case) * @return the found value, or null if not set */ @Contract("_, !null -> !null") public String getEnvVarOrUserProperty(@NotNull final String propertyName, @Nullable final String defaultValue) { return getConfigurable(propertyName, defaultValue, userProperties); } /** * Gets a configured setting from ~/.testcontainers.properties. * * @param propertyName name of configuration file property (dot-separated lower case) * @return the found value, or null if not set */ @Contract("_, !null -> !null") public String getUserProperty(@NotNull final String propertyName, @Nullable final String defaultValue) { return this.userProperties.get(propertyName) != null ? (String) this.userProperties.get(propertyName) : defaultValue; } /** * @return properties values available from user properties and classpath properties. Values set by environment * variable are NOT included. * @deprecated usages should be removed ASAP. See {@link TestcontainersConfiguration#getEnvVarOrProperty(String, String)}, * {@link TestcontainersConfiguration#getEnvVarOrUserProperty(String, String)} or {@link TestcontainersConfiguration#getUserProperty(String, String)} * for suitable replacements. */ @Deprecated public Properties getProperties() { return Stream .of(userProperties, classpathProperties) .reduce( new Properties(), (a, b) -> { a.putAll(b); return a; } ); } @Deprecated public boolean updateGlobalConfig(@NonNull String prop, @NonNull String value) { return updateUserConfig(prop, value); } @Synchronized public boolean updateUserConfig(@NonNull String prop, @NonNull String value) { try { if (value.equals(userProperties.get(prop))) { return false; } userProperties.setProperty(prop, value); USER_CONFIG_FILE.createNewFile(); try (OutputStream outputStream = new FileOutputStream(USER_CONFIG_FILE)) { userProperties.store(outputStream, "Modified by Testcontainers"); } // Update internal state only if environment config was successfully updated userProperties.setProperty(prop, value); return true; } catch (Exception e) { log.debug("Can't store environment property {} in {}", prop, USER_CONFIG_FILE); return false; } } @SneakyThrows(MalformedURLException.class) private static TestcontainersConfiguration loadConfiguration() { return new TestcontainersConfiguration( readProperties(USER_CONFIG_FILE.toURI().toURL()), ClasspathScanner .scanFor(PROPERTIES_FILE_NAME) .map(TestcontainersConfiguration::readProperties) .reduce( new Properties(), (a, b) -> { // first-write-wins merging - URLs appearing first on the classpath alphabetically will take priority. // Note that this means that file: URLs will always take priority over jar: URLs. b.putAll(a); return b; } ), System.getenv() ); } private static Properties readProperties(URL url) { log.debug("Testcontainers configuration overrides will be loaded from {}", url); Properties properties = new Properties(); try (InputStream inputStream = url.openStream()) { properties.load(inputStream); } catch (FileNotFoundException e) { log.debug( "Attempted to read Testcontainers configuration file at {} but the file was not found. Exception message: {}", url, ExceptionUtils.getRootCauseMessage(e) ); } catch (IOException e) { log.debug( "Attempted to read Testcontainers configuration file at {} but could it not be loaded. Exception message: {}", url, ExceptionUtils.getRootCauseMessage(e) ); } return properties; } private DockerImageName getImage(final String defaultValue) { return getConfiguredSubstituteImage(DockerImageName.parse(defaultValue)); } DockerImageName getConfiguredSubstituteImage(DockerImageName original) { for (final Map.Entry entry : CONTAINER_MAPPING.entrySet()) { if (original.isCompatibleWith(entry.getKey())) { return Optional .ofNullable(entry.getValue()) .map(propertyName -> getEnvVarOrProperty(propertyName, null)) .map(String::valueOf) .map(String::trim) .map(DockerImageName::parse) .orElse(original) .asCompatibleSubstituteFor(original); } } return original; } } ================================================ FILE: core/src/main/java/org/testcontainers/utility/ThrowingFunction.java ================================================ package org.testcontainers.utility; public interface ThrowingFunction { R apply(T t) throws Exception; } ================================================ FILE: core/src/main/java/org/testcontainers/utility/Versioning.java ================================================ package org.testcontainers.utility; import lombok.AccessLevel; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; /** * Represents mechanisms for versioning docker images. */ interface Versioning { AnyVersion ANY = new AnyVersion(); boolean isValid(); String getSeparator(); @NoArgsConstructor(access = AccessLevel.PRIVATE) class AnyVersion implements Versioning { @Override public boolean isValid() { return true; } @Override public String getSeparator() { return ":"; } @Override public String toString() { return "latest"; } @Override public boolean equals(final Object obj) { return obj instanceof Versioning; } @Override public int hashCode() { return super.hashCode(); } } @EqualsAndHashCode class TagVersioning implements Versioning { public static final String TAG_REGEX = "[\\w][\\w.\\-]{0,127}"; static final TagVersioning LATEST = new TagVersioning("latest"); private final String tag; TagVersioning(String tag) { this.tag = tag; } @Override public boolean isValid() { return tag.matches(TAG_REGEX); } @Override public String getSeparator() { return ":"; } @Override public String toString() { return tag; } } @EqualsAndHashCode class Sha256Versioning implements Versioning { public static final String HASH_REGEX = "[0-9a-fA-F]{32,}"; private final String hash; Sha256Versioning(String hash) { this.hash = hash; } @Override public boolean isValid() { return hash.matches(HASH_REGEX); } @Override public String getSeparator() { return "@"; } @Override public String toString() { return "sha256:" + hash; } } } ================================================ FILE: core/src/main/resources/META-INF/native-image/org.testcontainers/testcontainers/native-image.properties ================================================ Args = --enable-http --enable-https ================================================ FILE: core/src/main/resources/META-INF/services/org.testcontainers.dockerclient.DockerClientProviderStrategy ================================================ org.testcontainers.dockerclient.TestcontainersHostPropertyClientProviderStrategy org.testcontainers.dockerclient.EnvironmentAndSystemPropertyClientProviderStrategy org.testcontainers.dockerclient.UnixSocketClientProviderStrategy org.testcontainers.dockerclient.DockerMachineClientProviderStrategy org.testcontainers.dockerclient.NpipeSocketClientProviderStrategy org.testcontainers.dockerclient.RootlessDockerClientProviderStrategy org.testcontainers.dockerclient.DockerDesktopClientProviderStrategy ================================================ FILE: core/src/test/java/alt/testcontainers/README.md ================================================ Useful place for running tests that need to be outside of the `org.testcontainers` package. ================================================ FILE: core/src/test/java/alt/testcontainers/images/OutOfPackageImagePullPolicyTest.java ================================================ package alt.testcontainers.images; import org.junit.jupiter.api.Test; import org.testcontainers.TestImages; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; import org.testcontainers.images.AbstractImagePullPolicy; import org.testcontainers.images.ImageData; import org.testcontainers.utility.DockerImageName; class OutOfPackageImagePullPolicyTest { @Test void shouldSupportCustomPoliciesOutOfTestcontainersPackage() { try ( GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) .withImagePullPolicy( new AbstractImagePullPolicy() { @Override protected boolean shouldPullCached(DockerImageName imageName, ImageData localImageData) { return false; } } ) ) { container.withStartupCheckStrategy(new OneShotStartupCheckStrategy()); container.start(); } } } ================================================ FILE: core/src/test/java/org/testcontainers/DaemonTest.java ================================================ package org.testcontainers; import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import java.io.File; import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; /** * This test forks a new JVM, otherwise it's not possible to reliably diff the threads */ class DaemonTest { public static void main(String[] args) { Thread mainThread = Thread.currentThread(); GenericContainer genericContainer = null; try { genericContainer = new GenericContainer<>(TestImages.TINY_IMAGE).withCommand("top"); genericContainer.start(); Set threads = new HashSet<>(Thread.getAllStackTraces().keySet()); threads.remove(mainThread); Set nonDaemonThreads = threads.stream().filter(it -> !it.isDaemon()).collect(Collectors.toSet()); if (!nonDaemonThreads.isEmpty()) { String nonDaemonThreadNames = nonDaemonThreads .stream() .map(Thread::getName) .collect(Collectors.joining("\n", "\n", "")); fail("Expected all threads to be daemons but the following are not:\n" + nonDaemonThreadNames); } } finally { if (genericContainer != null) { genericContainer.stop(); } } } @Test void testThatAllThreadsAreDaemons() throws Exception { ProcessBuilder processBuilder = new ProcessBuilder( new File(System.getProperty("java.home")).toPath().resolve("bin").resolve("java").toString(), "-ea", "-classpath", System.getProperty("java.class.path"), DaemonTest.class.getCanonicalName() ); assertThat(processBuilder.inheritIO().start().waitFor()).isZero(); } } ================================================ FILE: core/src/test/java/org/testcontainers/DockerClientFactoryTest.java ================================================ package org.testcontainers; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.testcontainers.dockerclient.LogToStringContainerCallback; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MockTestcontainersConfigurationExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Test for {@link DockerClientFactory}. */ @ExtendWith(MockTestcontainersConfigurationExtension.class) class DockerClientFactoryTest { @Test void runCommandInsideDockerShouldNotFailIfImageDoesNotExistsLocally() { try (DockerRegistryContainer registryContainer = new DockerRegistryContainer()) { registryContainer.start(); DockerImageName imageName = registryContainer.createImage(); DockerClientFactory dockFactory = DockerClientFactory.instance(); dockFactory.runInsideDocker( imageName, cmd -> cmd.withCmd("sh", "-c", "echo 'SUCCESS'"), (client, id) -> { return client .logContainerCmd(id) .withStdOut(true) .exec(new LogToStringContainerCallback()) .toString(); } ); } } @Test void dockerHostIpAddress() { DockerClientFactory instance = new DockerClientFactory(); instance.strategy = null; assertThat(instance.dockerHostIpAddress()).isNotNull(); } @Test void failedChecksFailFast() { DockerClientFactory instance = DockerClientFactory.instance(); assertThat(instance.client()).isNotNull(); assertThat(instance.cachedClientFailure).isNull(); try { RuntimeException failure = new IllegalStateException("Boom!"); instance.cachedClientFailure = failure; // Fail fast assertThatThrownBy(instance::client).isEqualTo(failure); } finally { instance.cachedClientFailure = null; } } } ================================================ FILE: core/src/test/java/org/testcontainers/DockerRegistryContainer.java ================================================ package org.testcontainers; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.async.ResultCallback; import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.api.command.PullImageResultCallback; import lombok.Getter; import lombok.NonNull; import lombok.SneakyThrows; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.output.FrameConsumerResultCallback; import org.testcontainers.containers.output.OutputFrame; import org.testcontainers.containers.output.WaitingConsumer; import org.testcontainers.utility.Base58; import org.testcontainers.utility.DockerImageName; import java.util.UUID; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; public class DockerRegistryContainer extends GenericContainer { @Getter String endpoint; public DockerRegistryContainer() { super(TestImages.DOCKER_REGISTRY_IMAGE); } public DockerRegistryContainer(@NonNull Future image) { super(image); } @Override protected void configure() { super.configure(); withEnv("REGISTRY_HTTP_ADDR", "127.0.0.1:0"); withCreateContainerCmdModifier(cmd -> { cmd.getHostConfig().withNetworkMode("host"); }); } @Override @SneakyThrows protected void containerIsStarting(InspectContainerResponse containerInfo) { AtomicInteger port = new AtomicInteger(-1); try (FrameConsumerResultCallback resultCallback = new FrameConsumerResultCallback()) { WaitingConsumer waitingConsumer = new WaitingConsumer(); resultCallback.addConsumer(OutputFrame.OutputType.STDERR, waitingConsumer); dockerClient .logContainerCmd(containerInfo.getId()) .withStdErr(true) .withFollowStream(true) .exec(resultCallback); Pattern pattern = Pattern.compile( ".*listening on .*:(\\d+).*", Pattern.DOTALL | Pattern.CASE_INSENSITIVE | Pattern.MULTILINE ); waitingConsumer.waitUntil( it -> { String s = it.getUtf8String(); Matcher matcher = pattern.matcher(s); if (matcher.matches()) { port.set(Integer.parseInt(matcher.group(1))); return true; } else { return false; } }, 10, TimeUnit.SECONDS ); } endpoint = getHost() + ":" + port.get(); } public DockerImageName createImage() { return createImage(UUID.randomUUID().toString()); } public DockerImageName createImage(String tag) { return createImage("testcontainers/helloworld:latest", tag); } @SneakyThrows(InterruptedException.class) public DockerImageName createImage(String originalImage, String tag) { DockerClient client = getDockerClient(); client.pullImageCmd(originalImage).exec(new PullImageResultCallback()).awaitCompletion(); String dummyImageId = client.inspectImageCmd(originalImage).exec().getId(); DockerImageName imageName = DockerImageName .parse(getEndpoint() + "/" + Base58.randomString(6).toLowerCase()) .withTag(tag); // push the image to the registry client.tagImageCmd(dummyImageId, imageName.getUnversionedPart(), tag).exec(); client .pushImageCmd(imageName.asCanonicalNameString()) .exec(new ResultCallback.Adapter<>()) .awaitCompletion(1, TimeUnit.MINUTES); // Remove from local cache, tests should pull the image themselves client.removeImageCmd(imageName.asCanonicalNameString()).exec(); return imageName; } } ================================================ FILE: core/src/test/java/org/testcontainers/TestImages.java ================================================ package org.testcontainers; import org.testcontainers.utility.DockerImageName; public interface TestImages { DockerImageName REDIS_IMAGE = DockerImageName.parse("redis:6-alpine"); DockerImageName RABBITMQ_IMAGE = DockerImageName.parse("rabbitmq:3.7.25"); DockerImageName MONGODB_IMAGE = DockerImageName.parse("mongo:4.4"); DockerImageName ALPINE_IMAGE = DockerImageName.parse("alpine:3.17"); DockerImageName DOCKER_REGISTRY_IMAGE = DockerImageName.parse("registry:2.7.0"); DockerImageName TINY_IMAGE = DockerImageName.parse("alpine:3.17"); } ================================================ FILE: core/src/test/java/org/testcontainers/containers/ComposeContainerTest.java ================================================ package org.testcontainers.containers; import org.junit.jupiter.api.Test; import org.testcontainers.utility.DockerImageName; import java.io.File; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; class ComposeContainerTest { public static final String DOCKER_IMAGE = "docker:25.0.2"; private static final String COMPOSE_FILE_PATH = "src/test/resources/v2-compose-test.yml"; @Test void testWithCustomDockerImage() { ComposeContainer composeContainer = new ComposeContainer( DockerImageName.parse(DOCKER_IMAGE), new File(COMPOSE_FILE_PATH) ); composeContainer.start(); verifyContainerCreation(composeContainer); composeContainer.stop(); } @Test void testWithCustomDockerImageAndIdentifier() { ComposeContainer composeContainer = new ComposeContainer( DockerImageName.parse(DOCKER_IMAGE), "myidentifier", new File(COMPOSE_FILE_PATH) ); composeContainer.start(); verifyContainerCreation(composeContainer); composeContainer.stop(); } private void verifyContainerCreation(ComposeContainer composeContainer) { Optional redis = composeContainer.getContainerByServiceName("redis"); assertThat(redis) .hasValueSatisfying(container -> { assertThat(container.isRunning()).isTrue(); assertThat(container.getContainerInfo().getConfig().getLabels()) .containsEntry("com.docker.compose.version", "2.24.5"); }); } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/ComposeContainerWithServicesTest.java ================================================ package org.testcontainers.containers; import org.junit.jupiter.api.Test; import org.rnorth.ducttape.TimeoutException; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import java.io.File; import java.time.Duration; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; class ComposeContainerWithServicesTest { public static final File SIMPLE_COMPOSE_FILE = new File( "src/test/resources/compose-scaling-multiple-containers.yml" ); public static final File COMPOSE_FILE_WITH_INLINE_SCALE = new File( "src/test/resources/compose-with-inline-scale-test.yml" ); public static final File COMPOSE_FILE_WITH_HEALTHCHECK = new File( "src/test/resources/docker-compose-healthcheck.yml" ); @Test void testDesiredSubsetOfServicesAreStarted() { try ( ComposeContainer compose = new ComposeContainer(DockerImageName.parse("docker:25.0.5"), SIMPLE_COMPOSE_FILE) .withServices("redis") ) { compose.start(); verifyStartedContainers(compose, "redis-1"); } } @Test void testDesiredSubsetOfScaledServicesAreStarted() { try ( ComposeContainer compose = new ComposeContainer(DockerImageName.parse("docker:25.0.5"), SIMPLE_COMPOSE_FILE) .withScaledService("redis", 2) ) { compose.start(); verifyStartedContainers(compose, "redis-1", "redis-2"); } } @Test void testDesiredSubsetOfSpecifiedAndScaledServicesAreStarted() { try ( ComposeContainer compose = new ComposeContainer(DockerImageName.parse("docker:25.0.5"), SIMPLE_COMPOSE_FILE) .withServices("redis") .withScaledService("redis", 2) ) { compose.start(); verifyStartedContainers(compose, "redis-1", "redis-2"); } } @Test void testDesiredSubsetOfSpecifiedOrScaledServicesAreStarted() { try ( ComposeContainer compose = new ComposeContainer(DockerImageName.parse("docker:25.0.5"), SIMPLE_COMPOSE_FILE) .withServices("other") .withScaledService("redis", 2) ) { compose.start(); verifyStartedContainers(compose, "redis-1", "redis-2", "other-1"); } } @Test void testAllServicesAreStartedIfNotSpecified() { try ( ComposeContainer compose = new ComposeContainer(DockerImageName.parse("docker:25.0.5"), SIMPLE_COMPOSE_FILE) ) { compose.start(); verifyStartedContainers(compose, "redis-1", "other-1"); } } @Test void testScaleInComposeFileIsRespected() { try ( ComposeContainer compose = new ComposeContainer( DockerImageName.parse("docker:25.0.5"), COMPOSE_FILE_WITH_INLINE_SCALE ) ) { compose.start(); // the compose file includes `scale: 3` for the redis container verifyStartedContainers(compose, "redis-1", "redis-2", "redis-3"); } } @Test void testStartupTimeoutSetsTheHighestTimeout() { assertThat( catchThrowable(() -> { try ( ComposeContainer compose = new ComposeContainer( DockerImageName.parse("docker:25.0.5"), SIMPLE_COMPOSE_FILE ) .withServices("redis") .withStartupTimeout(Duration.ofMillis(1)) .withExposedService( "redis", 80, Wait.forListeningPort().withStartupTimeout(Duration.ofMinutes(1)) ); ) { compose.start(); } }) ) .as("We expect a timeout from the startup timeout") .isInstanceOf(TimeoutException.class); } @Test void testWaitingForHealthcheck() { try ( ComposeContainer compose = new ComposeContainer( DockerImageName.parse("docker:25.0.5"), COMPOSE_FILE_WITH_HEALTHCHECK ) .waitingFor("redis", Wait.forHealthcheck().withStartupTimeout(Duration.ofMinutes(2))) ) { compose.start(); verifyStartedContainers(compose, "redis-1"); } } @Test void testWaitingForHealthcheckWithRestartDoesNotCrash() { try ( ComposeContainer compose = new ComposeContainer( DockerImageName.parse("docker:25.0.5"), COMPOSE_FILE_WITH_HEALTHCHECK ) .waitingFor("redis", Wait.forHealthcheck().withStartupTimeout(Duration.ofMinutes(1))) ) { compose.start(); compose.stop(); compose.start(); verifyStartedContainers(compose, "redis-1"); } } private void verifyStartedContainers(final ComposeContainer compose, final String... names) { final List containerNames = compose .listChildContainers() .stream() .flatMap(container -> Stream.of(container.getNames())) .collect(Collectors.toList()); assertThat(containerNames) .as("number of running services of docker-compose is the same as length of listOfServices") .hasSize(names.length); for (final String expectedName : names) { final long matches = containerNames.stream().filter(foundName -> foundName.endsWith(expectedName)).count(); assertThat(matches) .as("container with name starting '" + expectedName + "' should be running") .isEqualTo(1L); } } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/ComposeOverridesTest.java ================================================ package org.testcontainers.containers; import com.google.common.util.concurrent.Uninterruptibles; import org.apache.commons.lang3.SystemUtils; import org.assertj.core.api.Assumptions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.rnorth.ducttape.unreliables.Unreliables; import org.testcontainers.utility.CommandLine; import org.testcontainers.utility.DockerImageName; import java.io.BufferedReader; import java.io.File; import java.io.InputStreamReader; import java.net.Socket; import java.util.Arrays; import java.util.concurrent.TimeUnit; class ComposeOverridesTest { private static final String DOCKER_EXECUTABLE = SystemUtils.IS_OS_WINDOWS ? "docker.exe" : "docker"; private static final File BASE_COMPOSE_FILE = new File("src/test/resources/docker-compose-base.yml"); private static final String BASE_ENV_VAR = "bar=base"; private static final File OVERRIDE_COMPOSE_FILE = new File( "src/test/resources/docker-compose-non-default-override.yml" ); private static final String OVERRIDE_ENV_VAR = "bar=overwritten"; private static final int SERVICE_PORT = 3000; private static final String SERVICE_NAME = "alpine-1"; public static Iterable data() { return Arrays.asList( new Object[][] { { true, BASE_ENV_VAR, new File[] { BASE_COMPOSE_FILE } }, { true, OVERRIDE_ENV_VAR, new File[] { BASE_COMPOSE_FILE, OVERRIDE_COMPOSE_FILE } }, { false, BASE_ENV_VAR, new File[] { BASE_COMPOSE_FILE } }, { false, OVERRIDE_ENV_VAR, new File[] { BASE_COMPOSE_FILE, OVERRIDE_COMPOSE_FILE } }, } ); } @ParameterizedTest(name = "{index}: local[{0}], composeFiles[{2}], expectedEnvVar[{1}]") @MethodSource("data") void test(boolean localMode, String expectedEnvVar, File... composeFiles) { ComposeContainer compose; if (localMode) { Assumptions .assumeThat(CommandLine.executableExists(DOCKER_EXECUTABLE)) .as("docker executable exists") .isTrue(); compose = new ComposeContainer(composeFiles).withExposedService(SERVICE_NAME, SERVICE_PORT); } else { compose = new ComposeContainer(DockerImageName.parse("docker:25.0.2"), composeFiles) .withExposedService(SERVICE_NAME, SERVICE_PORT); } compose.start(); BufferedReader br = Unreliables.retryUntilSuccess( 10, TimeUnit.SECONDS, () -> { Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); Socket socket = new Socket( compose.getServiceHost(SERVICE_NAME, SERVICE_PORT), compose.getServicePort(SERVICE_NAME, SERVICE_PORT) ); return new BufferedReader(new InputStreamReader(socket.getInputStream())); } ); Unreliables.retryUntilTrue( 10, TimeUnit.SECONDS, () -> { while (br.ready()) { String line = br.readLine(); if (line.contains(expectedEnvVar)) { return true; } } Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS); return false; } ); compose.stop(); } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/ComposeProfilesOptionTest.java ================================================ package org.testcontainers.containers; import org.assertj.core.api.Assumptions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.testcontainers.utility.CommandLine; import org.testcontainers.utility.DockerImageName; import java.io.File; import static org.assertj.core.api.Assertions.assertThat; class ComposeProfilesOptionTest { public static Boolean[] local() { return new Boolean[] { Boolean.TRUE, Boolean.FALSE }; } public static final File COMPOSE_FILE = new File("src/test/resources/compose-profile-option/compose-test.yml"); @ParameterizedTest @MethodSource("local") void testProfileOption(boolean localMode) { ComposeContainer compose; if (localMode) { Assumptions .assumeThat(CommandLine.executableExists(ComposeContainer.COMPOSE_EXECUTABLE)) .as("docker executable exists") .isTrue(); // composeContainerWithLocalCompose { compose = new ComposeContainer(COMPOSE_FILE) // } .withOptions("--profile=cache"); } else { compose = new ComposeContainer(DockerImageName.parse("docker:25.0.2"), COMPOSE_FILE) .withOptions("--profile=cache"); } compose.start(); assertThat(compose.listChildContainers()).hasSize(1); compose.stop(); } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/ContainerStateTest.java ================================================ package org.testcontainers.containers; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import java.util.Collections; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class ContainerStateTest { public static Object[][] params() { return new Object[][] { new Object[] { "regular mapping", "80:8080/tcp", Collections.singletonList(80) }, new Object[] { "regular mapping with host", "127.0.0.1:80:8080/tcp", Collections.singletonList(80) }, new Object[] { "zero port without host", ":0:8080/tcp", Collections.emptyList() }, new Object[] { "missing port with host", "0.0.0.0:0:8080/tcp", Collections.emptyList() }, new Object[] { "zero port (synthetic case)", "0:8080/tcp", Collections.emptyList() }, new Object[] { "missing port", ":8080/tcp", Collections.emptyList() }, }; } @ParameterizedTest(name = "{0} ({1} -> {2})") @MethodSource("params") void test(String name, String testSet, List expectedResult) { ContainerState containerState = mock(ContainerState.class); doCallRealMethod().when(containerState).getBoundPortNumbers(); when(containerState.getPortBindings()).thenReturn(Collections.singletonList(testSet)); List result = containerState.getBoundPortNumbers(); assertThat(result).hasSameElementsAs(expectedResult); } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/DockerComposeContainerCustomImageTest.java ================================================ package org.testcontainers.containers; import org.junit.jupiter.api.Test; import org.testcontainers.utility.DockerImageName; import java.io.File; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; class DockerComposeContainerCustomImageTest { public static final String DOCKER_IMAGE = "docker/compose:debian-1.29.2"; private static final String COMPOSE_FILE_PATH = "src/test/resources/scaled-compose-test.yml"; @Test void testWithCustomDockerImage() { DockerComposeContainer composeContainer = new DockerComposeContainer<>( DockerImageName.parse(DOCKER_IMAGE), "testing", new File(COMPOSE_FILE_PATH) ); composeContainer.start(); verifyContainerCreation(composeContainer); composeContainer.stop(); } @Test void testWithCustomDockerImageAndIdentifier() { DockerComposeContainer composeContainer = new DockerComposeContainer( DockerImageName.parse(DOCKER_IMAGE), "myidentifier", new File(COMPOSE_FILE_PATH) ); composeContainer.start(); verifyContainerCreation(composeContainer); composeContainer.stop(); } private void verifyContainerCreation(DockerComposeContainer composeContainer) { Optional redis = composeContainer.getContainerByServiceName("redis"); assertThat(redis) .hasValueSatisfying(container -> { assertThat(container.isRunning()).isTrue(); assertThat(container.getContainerInfo().getConfig().getLabels()) .containsEntry("com.docker.compose.version", "1.29.2"); }); } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/DockerComposeContainerWithServicesTest.java ================================================ package org.testcontainers.containers; import org.junit.jupiter.api.Test; import org.rnorth.ducttape.TimeoutException; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import java.io.File; import java.time.Duration; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; class DockerComposeContainerWithServicesTest { public static final File SIMPLE_COMPOSE_FILE = new File( "src/test/resources/compose-scaling-multiple-containers.yml" ); public static final File COMPOSE_FILE_WITH_INLINE_SCALE = new File( "src/test/resources/compose-with-inline-scale-test.yml" ); public static final File COMPOSE_FILE_WITH_HEALTHCHECK = new File( "src/test/resources/docker-compose-healthcheck.yml" ); @Test void testDesiredSubsetOfServicesAreStarted() { try ( DockerComposeContainer compose = new DockerComposeContainer<>( DockerImageName.parse("docker/compose:debian-1.29.2"), SIMPLE_COMPOSE_FILE ) .withServices("redis") ) { compose.start(); verifyStartedContainers(compose, "redis_1"); } } @Test void testDesiredSubsetOfScaledServicesAreStarted() { try ( DockerComposeContainer compose = new DockerComposeContainer<>( DockerImageName.parse("docker/compose:debian-1.29.2"), SIMPLE_COMPOSE_FILE ) .withScaledService("redis", 2) ) { compose.start(); verifyStartedContainers(compose, "redis_1", "redis_2"); } } @Test void testDesiredSubsetOfSpecifiedAndScaledServicesAreStarted() { try ( DockerComposeContainer compose = new DockerComposeContainer<>( DockerImageName.parse("docker/compose:debian-1.29.2"), SIMPLE_COMPOSE_FILE ) .withServices("redis") .withScaledService("redis", 2) ) { compose.start(); verifyStartedContainers(compose, "redis_1", "redis_2"); } } @Test void testDesiredSubsetOfSpecifiedOrScaledServicesAreStarted() { try ( DockerComposeContainer compose = new DockerComposeContainer<>( DockerImageName.parse("docker/compose:debian-1.29.2"), SIMPLE_COMPOSE_FILE ) .withServices("other") .withScaledService("redis", 2) ) { compose.start(); verifyStartedContainers(compose, "redis_1", "redis_2", "other_1"); } } @Test void testAllServicesAreStartedIfNotSpecified() { try ( DockerComposeContainer compose = new DockerComposeContainer<>( DockerImageName.parse("docker/compose:debian-1.29.2"), SIMPLE_COMPOSE_FILE ) ) { compose.start(); verifyStartedContainers(compose, "redis_1", "other_1"); } } @Test void testScaleInComposeFileIsRespected() { try ( DockerComposeContainer compose = new DockerComposeContainer<>( DockerImageName.parse("docker/compose:debian-1.29.2"), COMPOSE_FILE_WITH_INLINE_SCALE ) ) { compose.start(); // the compose file includes `scale: 3` for the redis container verifyStartedContainers(compose, "redis_1", "redis_2", "redis_3"); } } @Test void testStartupTimeoutSetsTheHighestTimeout() { assertThat( catchThrowable(() -> { try ( DockerComposeContainer compose = new DockerComposeContainer<>( DockerImageName.parse("docker/compose:debian-1.29.2"), SIMPLE_COMPOSE_FILE ) .withServices("redis") .withStartupTimeout(Duration.ofMillis(1)) .withExposedService( "redis", 80, Wait.forListeningPort().withStartupTimeout(Duration.ofMinutes(1)) ); ) { compose.start(); } }) ) .as("We expect a timeout from the startup timeout") .isInstanceOf(TimeoutException.class); } @Test void testWaitingForHealthcheck() { try ( DockerComposeContainer compose = new DockerComposeContainer<>( DockerImageName.parse("docker/compose:debian-1.29.2"), COMPOSE_FILE_WITH_HEALTHCHECK ) .waitingFor("redis", Wait.forHealthcheck().withStartupTimeout(Duration.ofMinutes(2))) ) { compose.start(); verifyStartedContainers(compose, "redis_1"); } } @Test void testWaitingForHealthcheckWithRestartDoesNotCrash() { try ( DockerComposeContainer compose = new DockerComposeContainer<>( DockerImageName.parse("docker/compose:debian-1.29.2"), COMPOSE_FILE_WITH_HEALTHCHECK ) .waitingFor("redis", Wait.forHealthcheck().withStartupTimeout(Duration.ofMinutes(1))) ) { compose.start(); compose.stop(); compose.start(); verifyStartedContainers(compose, "redis_1"); } } private void verifyStartedContainers(final DockerComposeContainer compose, final String... names) { final List containerNames = compose .listChildContainers() .stream() .flatMap(container -> Stream.of(container.getNames())) .collect(Collectors.toList()); assertThat(containerNames) .as("number of running services of docker-compose is the same as length of listOfServices") .hasSize(names.length); for (final String expectedName : names) { final long matches = containerNames.stream().filter(foundName -> foundName.endsWith(expectedName)).count(); assertThat(matches) .as("container with name starting '" + expectedName + "' should be running") .isEqualTo(1L); } } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/DockerComposeFilesTest.java ================================================ package org.testcontainers.containers; import com.google.common.collect.Lists; import org.junit.jupiter.api.Test; import java.io.File; import static org.assertj.core.api.Assertions.assertThat; class DockerComposeFilesTest { @Test void shouldGetDependencyImages() { DockerComposeFiles dockerComposeFiles = new DockerComposeFiles( Lists.newArrayList(new File("src/test/resources/docker-compose-imagename-parsing-v2.yml")) ); assertThat(dockerComposeFiles.getDependencyImages()) .containsExactlyInAnyOrder("postgres:latest", "redis:latest", "mysql:latest"); } @Test void shouldGetDependencyImagesWhenOverriding() { DockerComposeFiles dockerComposeFiles = new DockerComposeFiles( Lists.newArrayList( new File("src/test/resources/docker-compose-imagename-overriding-a.yml"), new File("src/test/resources/docker-compose-imagename-overriding-b.yml") ) ); assertThat(dockerComposeFiles.getDependencyImages()) .containsExactlyInAnyOrder("alpine:3.17", "redis:b", "mysql:b", "aservice:latest"); } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/DockerComposeOverridesTest.java ================================================ package org.testcontainers.containers; import com.google.common.util.concurrent.Uninterruptibles; import org.assertj.core.api.Assumptions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.rnorth.ducttape.unreliables.Unreliables; import org.testcontainers.utility.CommandLine; import org.testcontainers.utility.DockerImageName; import java.io.BufferedReader; import java.io.File; import java.io.InputStreamReader; import java.net.Socket; import java.util.Arrays; import java.util.concurrent.TimeUnit; class DockerComposeOverridesTest { private static final File BASE_COMPOSE_FILE = new File("src/test/resources/docker-compose-base.yml"); private static final String BASE_ENV_VAR = "bar=base"; private static final File OVERRIDE_COMPOSE_FILE = new File( "src/test/resources/docker-compose-non-default-override.yml" ); private static final String OVERRIDE_ENV_VAR = "bar=overwritten"; private static final int SERVICE_PORT = 3000; private static final String SERVICE_NAME = "alpine_1"; public static Iterable data() { return Arrays.asList( new Object[][] { { true, BASE_ENV_VAR, new File[] { BASE_COMPOSE_FILE } }, { true, OVERRIDE_ENV_VAR, new File[] { BASE_COMPOSE_FILE, OVERRIDE_COMPOSE_FILE } }, { false, BASE_ENV_VAR, new File[] { BASE_COMPOSE_FILE } }, { false, OVERRIDE_ENV_VAR, new File[] { BASE_COMPOSE_FILE, OVERRIDE_COMPOSE_FILE } }, } ); } @ParameterizedTest(name = "{index}: local[{0}], composeFiles[{2}], expectedEnvVar[{1}]") @MethodSource("data") void test(boolean localMode, String expectedEnvVar, File... composeFiles) { DockerComposeContainer compose; if (localMode) { Assumptions .assumeThat(CommandLine.executableExists(DockerComposeContainer.COMPOSE_EXECUTABLE)) .as("docker-compose executable exists") .isTrue(); Assumptions .assumeThat(CommandLine.runShellCommand("docker-compose", "--version")) .doesNotStartWith("Docker Compose version v2"); compose = new DockerComposeContainer(composeFiles).withExposedService(SERVICE_NAME, SERVICE_PORT); } else { compose = new DockerComposeContainer(DockerImageName.parse("docker/compose:debian-1.29.2"), composeFiles) .withExposedService(SERVICE_NAME, SERVICE_PORT); } compose.start(); BufferedReader br = Unreliables.retryUntilSuccess( 10, TimeUnit.SECONDS, () -> { Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); Socket socket = new Socket( compose.getServiceHost(SERVICE_NAME, SERVICE_PORT), compose.getServicePort(SERVICE_NAME, SERVICE_PORT) ); return new BufferedReader(new InputStreamReader(socket.getInputStream())); } ); Unreliables.retryUntilTrue( 10, TimeUnit.SECONDS, () -> { while (br.ready()) { String line = br.readLine(); if (line.contains(expectedEnvVar)) { return true; } } Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS); return false; } ); compose.stop(); } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/DockerComposeProfilesOptionTest.java ================================================ package org.testcontainers.containers; import org.assertj.core.api.Assumptions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.testcontainers.utility.CommandLine; import org.testcontainers.utility.DockerImageName; import java.io.File; import static org.assertj.core.api.Assertions.assertThat; class DockerComposeProfilesOptionTest { public static Boolean[] local() { return new Boolean[] { Boolean.TRUE, Boolean.FALSE }; } public static final File COMPOSE_FILE = new File("src/test/resources/compose-profile-option/compose-test.yml"); @ParameterizedTest(name = "{0}") @MethodSource("local") void testProfileOption(boolean localMode) { DockerComposeContainer compose; if (localMode) { Assumptions .assumeThat(CommandLine.executableExists(DockerComposeContainer.COMPOSE_EXECUTABLE)) .as("docker-compose executable exists") .isTrue(); Assumptions .assumeThat(CommandLine.runShellCommand("docker-compose", "--version")) .doesNotStartWith("Docker Compose version v2"); compose = new DockerComposeContainer<>(COMPOSE_FILE).withOptions("--profile=cache"); } else { compose = new DockerComposeContainer<>(DockerImageName.parse("docker/compose:debian-1.29.2"), COMPOSE_FILE) .withOptions("--profile=cache"); } compose.start(); assertThat(compose.listChildContainers()).hasSize(1); compose.stop(); } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/DockerMcpGatewayContainerTest.java ================================================ package org.testcontainers.containers; import org.junit.jupiter.api.Test; import java.util.Collections; import static org.assertj.core.api.Assertions.assertThat; class DockerMcpGatewayContainerTest { @Test void serviceSuccessfullyStarts() { try (DockerMcpGatewayContainer gateway = new DockerMcpGatewayContainer("docker/mcp-gateway:latest")) { gateway.start(); assertThat(gateway.isRunning()).isTrue(); } } @Test void gatewayStartsWithServers() { try ( // container { DockerMcpGatewayContainer gateway = new DockerMcpGatewayContainer("docker/mcp-gateway:latest") .withServer("curl", "curl") .withServer("brave", "brave_local_search", "brave_web_search") .withServer("github-official", Collections.singletonList("add_issue_comment")) .withSecret("brave.api_key", "test_key") .withSecrets(Collections.singletonMap("github.personal_access_token", "test_token")) // } ) { gateway.start(); assertThat(gateway.getLogs()).contains("4 tools listed"); } } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/DockerModelRunnerContainerTest.java ================================================ package org.testcontainers.containers; import io.restassured.RestAssured; import io.restassured.response.Response; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assumptions.assumeThat; class DockerModelRunnerContainerTest { @Test void checkStatus() { assumeThat(System.getenv("CI")).isNull(); try ( // container { DockerModelRunnerContainer dmr = new DockerModelRunnerContainer("alpine/socat:1.7.4.3-r0") // } ) { dmr.start(); Response modelResponse = RestAssured.get(dmr.getBaseEndpoint() + "/status").thenReturn(); assertThat(modelResponse.body().asString()).contains("The service is running"); } } @Test void pullsModelAndExposesInference() { assumeThat(System.getenv("CI")).isNull(); String modelName = "ai/smollm2:360M-Q4_K_M"; try ( // pullModel { DockerModelRunnerContainer dmr = new DockerModelRunnerContainer("alpine/socat:1.7.4.3-r0") .withModel(modelName) // } ) { dmr.start(); Response modelResponse = RestAssured.get(dmr.getBaseEndpoint() + "/models").thenReturn(); assertThat(modelResponse.body().jsonPath().getList("tags.flatten()")).contains(modelName); Response openAiResponse = RestAssured.get(dmr.getOpenAIEndpoint() + "/v1/models").prettyPeek().thenReturn(); assertThat(openAiResponse.body().jsonPath().getList("data.id")).contains(modelName); } } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/ExposedHostTest.java ================================================ package org.testcontainers.containers; import com.google.common.collect.ImmutableMap; import com.sun.net.httpserver.HttpServer; import lombok.SneakyThrows; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; 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; class ExposedHostTest { private static HttpServer server; @BeforeAll public static void setUpClass() throws Exception { server = HttpServer.create(new InetSocketAddress(0), 0); server.createContext( "/", exchange -> { byte[] content = "Hello World!".getBytes(); exchange.sendResponseHeaders(200, content.length); try (OutputStream responseBody = exchange.getResponseBody()) { responseBody.write(content); responseBody.flush(); } } ); server.start(); } @AfterAll public static void tearDownClass() { server.stop(0); } @AfterEach public void tearDown() { PortForwardingContainer.INSTANCE.reset(); } @Test void testExposedHostAfterContainerIsStarted() { try (GenericContainer container = new GenericContainer<>(tinyContainerDef()).withAccessToHost(true)) { container.start(); Testcontainers.exposeHostPorts(server.getAddress().getPort()); assertResponse(container, server.getAddress().getPort()); } } @Test void testExposedHost() { Testcontainers.exposeHostPorts(server.getAddress().getPort()); assertResponse(new GenericContainer<>(tinyContainerDef()), server.getAddress().getPort()); } @Test void testExposedHostWithNetwork() { Testcontainers.exposeHostPorts(server.getAddress().getPort()); try (Network network = Network.newNetwork()) { assertResponse( new GenericContainer<>(tinyContainerDef()).withNetwork(network), server.getAddress().getPort() ); } } @Test void testExposedHostPortOnFixedInternalPorts() { Testcontainers.exposeHostPorts(ImmutableMap.of(server.getAddress().getPort(), 80)); Testcontainers.exposeHostPorts(ImmutableMap.of(server.getAddress().getPort(), 81)); assertResponse(new GenericContainer<>(tinyContainerDef()), 80); assertResponse(new GenericContainer<>(tinyContainerDef()), 81); } @Test 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 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 { container.start(); String response = container .execInContainer("wget", "-O", "-", "http://host.testcontainers.internal:" + port) .getStdout(); assertThat(response).as("received response").isEqualTo("Hello World!"); } finally { container.stop(); } } private ContainerDef tinyContainerDef() { return new TinyContainerDef(); } private static class TinyContainerDef extends ContainerDef { TinyContainerDef() { setImage(TestImages.TINY_IMAGE); 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() {} }; List networks = DockerClientFactory .lazyClient() .listNetworksCmd() .withNameFilter(networkName) .exec(); if (networks.isEmpty()) { Network.builder().createNetworkCmdModifier(cmd -> cmd.withName(networkName)).build().getId(); } return network; } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/GenericContainerTest.java ================================================ package org.testcontainers.containers; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.read.ListAppender; 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; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; import org.assertj.core.api.Assumptions; import org.junit.jupiter.api.Test; import org.rnorth.ducttape.unreliables.Unreliables; import org.testcontainers.DockerClientFactory; import org.testcontainers.TestImages; import org.testcontainers.containers.startupcheck.StartupCheckStrategy; import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy; import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.images.RemoteDockerImage; import org.testcontainers.images.builder.ImageFromDockerfile; import org.testcontainers.images.builder.Transferable; 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; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.catchThrowable; import static org.assertj.core.api.Assumptions.assumeThat; class GenericContainerTest { @Test void shouldReportOOMAfterWait() { Info info = DockerClientFactory.instance().client().infoCmd().exec(); // Poor man's rootless Docker detection :D Assumptions.assumeThat(info.getSecurityOptions()).doesNotContain("name=rootless"); try ( GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) .withStartupCheckStrategy(new NoopStartupCheckStrategy()) .waitingFor(new WaitForExitedState(ContainerState::getOOMKilled)) .withCreateContainerCmdModifier(it -> { it .getHostConfig() .withMemory(20 * FileUtils.ONE_MB) .withMemorySwappiness(0L) .withMemorySwap(0L) .withMemoryReservation(0L) .withKernelMemory(16 * FileUtils.ONE_MB); }) .withCommand("sh", "-c", "A='0123456789'; for i in $(seq 0 32); do A=$A$A; done; sleep 10m") ) { assertThatThrownBy(container::start) .hasStackTraceContaining("Wait strategy failed. Container crashed with out-of-memory (OOMKilled)") .hasStackTraceContaining("Nope!"); } } @Test void shouldReportErrorAfterWait() { try ( GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) .withStartupCheckStrategy(new NoopStartupCheckStrategy()) .waitingFor(new WaitForExitedState(state -> state.getExitCode() > 0)) .withCommand("sh", "-c", "usleep 100; exit 123") ) { assertThatThrownBy(container::start) .hasStackTraceContaining("Container startup failed for image " + TestImages.TINY_IMAGE) .hasStackTraceContaining("Wait strategy failed. Container exited with code 123") .hasStackTraceContaining("Nope!"); } } @Test void shouldCopyTransferableAsFile() { try ( // transferableFile { GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) .withStartupCheckStrategy(new NoopStartupCheckStrategy()) .withCopyToContainer(Transferable.of("test"), "/tmp/test") .waitingFor(new WaitForExitedState(state -> state.getExitCodeLong() > 0)) .withCommand("sh", "-c", "grep -q test /tmp/test && exit 100") // } ) { assertThatThrownBy(container::start) .hasStackTraceContaining("Wait strategy failed. Container exited with code 100") .hasStackTraceContaining("Nope!"); } } @Test void shouldCopyTransferableAsFileWithFileMode() { try ( // transferableWithFileMode { GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) .withStartupCheckStrategy(new NoopStartupCheckStrategy()) .withCopyToContainer(Transferable.of("test", 0777), "/tmp/test") .waitingFor(new WaitForExitedState(state -> state.getExitCodeLong() > 0)) .withCommand("sh", "-c", "ls -ll /tmp | grep '\\-rwxrwxrwx\\|test' && exit 100") // } ) { assertThatThrownBy(container::start) .hasStackTraceContaining("Wait strategy failed. Container exited with code 100") .hasStackTraceContaining("Nope!"); } } @Test void shouldCopyTransferableAfterMountableFile() { try ( GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) .withStartupCheckStrategy(new NoopStartupCheckStrategy()) .withCopyFileToContainer(MountableFile.forClasspathResource("test_copy_to_container.txt"), "/tmp/test") .withCopyToContainer(Transferable.of("test"), "/tmp/test") .waitingFor(new WaitForExitedState(state -> state.getExitCodeLong() > 0)) .withCommand("sh", "-c", "grep -q test /tmp/test && exit 100") ) { assertThatThrownBy(container::start) .hasStackTraceContaining("Wait strategy failed. Container exited with code 100") .hasStackTraceContaining("Nope!"); } } @Test void shouldOnlyPublishExposedPorts() { ImageFromDockerfile image = new ImageFromDockerfile("publish-multiple") .withDockerfileFromBuilder(builder -> { builder .from("testcontainers/helloworld:1.1.0") // .expose(8080, 8081) .build(); }); try (GenericContainer container = new GenericContainer<>(image).withExposedPorts(8080)) { container.start(); InspectContainerResponse inspectedContainer = container.getContainerInfo(); List exposedPorts = Arrays .stream(inspectedContainer.getConfig().getExposedPorts()) .map(ExposedPort::getPort) .collect(Collectors.toList()); assertThat(exposedPorts).as("the exposed ports are all of those EXPOSEd by the image").contains(8080, 8081); Map hostBindings = inspectedContainer .getHostConfig() .getPortBindings() .getBindings(); assertThat(hostBindings).as("only 1 port is bound on the host (published)").hasSize(1); Integer mappedPort = container.getMappedPort(8080); assertThat(mappedPort != 8080).as("port 8080 is bound to a different port on the host").isTrue(); assertThat(catchThrowable(() -> container.getMappedPort(8081))) .as("trying to get a non-bound port mapping fails") .isInstanceOf(IllegalArgumentException.class); } } @Test void shouldWaitUntilExposedPortIsMapped() { ImageFromDockerfile image = new ImageFromDockerfile("publish-multiple") .withDockerfileFromBuilder(builder -> { builder .from("testcontainers/helloworld:1.1.0") .expose(8080, 8081) // one additional port exposed in image .build(); }); try ( GenericContainer container = new GenericContainer<>(image) .withExposedPorts(8080) .withCreateContainerCmdModifier(it -> it.withExposedPorts(ExposedPort.tcp(8082))) // another port exposed by modifier ) { container.start(); assertThat(container.getExposedPorts()).as("Only withExposedPort should be exposed").hasSize(1); assertThat(container.getExposedPorts()).as("withExposedPort should be exposed").contains(8080); } } @Test void testArchitectureCheck() { assumeThat(DockerClientFactory.instance().client().versionCmd().exec().getArch()).isNotEqualTo("amd64"); // Choose an image that is *different* from the server architecture--this ensures we always get a warning. final String image; if (DockerClientFactory.instance().client().versionCmd().exec().getArch().equals("amd64")) { // arm64 image image = "testcontainers/sshd@sha256:f701fa4ae2cd25ad2b2ea2df1aad00980f67bacdd03958a2d7d52ee63d7fb3e8"; } else { // amd64 image image = "testcontainers/sshd@sha256:7879c6c99eeab01f1c6beb2c240d49a70430ef2d52f454765ec9707f547ef6f1"; } try (GenericContainer container = new GenericContainer<>(image)) { // Grab a copy of everything that is logged when we start the container ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) container.logger(); ListAppender listAppender = new ListAppender<>(); listAppender.start(); logger.addAppender(listAppender); container.start(); String regexMatch = "The architecture '\\S+' for image .*"; assertThat(listAppender.list) .describedAs( "Received log list does not have a message matching '" + regexMatch + "': " + listAppender.list.toString() ) .filteredOn(event -> event.getMessage().matches(regexMatch)) .isNotEmpty(); } } @Test void shouldReturnTheProvidedImage() { GenericContainer container = new GenericContainer(TestImages.REDIS_IMAGE); assertThat(container.getImage().get()).isEqualTo("redis:6-alpine"); container.setImage(new RemoteDockerImage(TestImages.ALPINE_IMAGE)); assertThat(container.getImage().get()).isEqualTo("alpine:3.17"); } @Test void shouldContainDefaultNetworkAlias() { try (GenericContainer container = new GenericContainer<>("testcontainers/helloworld:1.1.0")) { container.start(); assertThat(container.getNetworkAliases()).hasSize(1); } } @Test void shouldContainDefaultNetworkAliasWhenUsingGenericContainer() { try (HelloWorldContainer container = new HelloWorldContainer("testcontainers/helloworld:1.1.0")) { container.start(); assertThat(container.getNetworkAliases()).hasSize(1); } } @Test void shouldContainDefaultNetworkAliasWhenUsingContainerDef() { try (TcHelloWorldContainer container = new TcHelloWorldContainer("testcontainers/helloworld:1.1.0")) { container.start(); assertThat(container.getNetworkAliases()).hasSize(1); } } @Test void shouldRespectWaitStrategy() { try ( HelloWorldLogStrategyContainer container = new HelloWorldLogStrategyContainer( "testcontainers/helloworld:1.1.0" ) ) { container.setWaitStrategy(Wait.forLogMessage(".*Starting server on port.*", 1)); container.start(); assertThat((LogMessageWaitStrategy) container.getWaitStrategy()) .extracting("regEx", "times") .containsExactly(".*Starting server on port.*", 1); } } @Test 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 public StartupStatus checkStartupState(DockerClient dockerClient, String containerId) { return StartupStatus.SUCCESSFUL; } } @RequiredArgsConstructor @FieldDefaults(makeFinal = true) @Slf4j static class WaitForExitedState extends AbstractWaitStrategy { Predicate predicate; @Override @SneakyThrows protected void waitUntilReady() { Unreliables.retryUntilTrue( 5, TimeUnit.SECONDS, () -> { ContainerState state = waitStrategyTarget.getCurrentContainerInfo().getState(); log.debug("Current state: {}", state); if (!"exited".equalsIgnoreCase(state.getStatus())) { Thread.sleep(100); return false; } return predicate.test(state); } ); throw new IllegalStateException("Nope!"); } } static class HelloWorldContainer extends GenericContainer { public HelloWorldContainer(String image) { super(DockerImageName.parse(image)); withExposedPorts(8080); } } static class TcHelloWorldContainer extends GenericContainer { public TcHelloWorldContainer(String image) { super(DockerImageName.parse(image)); } @Override ContainerDef createContainerDef() { return new HelloWorldContainerDef(); } class HelloWorldContainerDef extends ContainerDef { HelloWorldContainerDef() { addExposedTcpPort(8080); } } } static class HelloWorldLogStrategyContainer extends GenericContainer { public HelloWorldLogStrategyContainer(String image) { super(DockerImageName.parse(image)); withExposedPorts(8080); waitingFor(Wait.forLogMessage(".*Starting server on port.*", 2)); } } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/JibTest.java ================================================ package org.testcontainers.containers; import com.github.dockerjava.api.command.InspectImageResponse; import org.junit.jupiter.api.Test; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.output.OutputFrame.OutputType; import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; import org.testcontainers.jib.JibImage; import java.time.Duration; import java.util.Collections; import static org.assertj.core.api.Assertions.assertThat; class JibTest { @Test void buildImage() { try ( // jibContainerUsage { GenericContainer busybox = new GenericContainer<>( new JibImage( "busybox:1.35", jibContainerBuilder -> { return jibContainerBuilder.setEntrypoint("echo", "Hello World"); } ) ) .withStartupCheckStrategy(new OneShotStartupCheckStrategy().withTimeout(Duration.ofSeconds(3))) // } ) { busybox.start(); String logs = busybox.getLogs(OutputType.STDOUT); assertThat(logs).contains("Hello World"); } } @Test void standardLabelsAreAddedWhenUsingJibSetLabels() { try ( GenericContainer busybox = new GenericContainer<>( new JibImage( "busybox:1.35", jibContainerBuilder -> { return jibContainerBuilder .setEntrypoint("echo", "Hello World") .setLabels(Collections.singletonMap("foo", "bar")); } ) ) .withStartupCheckStrategy(new OneShotStartupCheckStrategy().withTimeout(Duration.ofSeconds(3))) ) { busybox.start(); assertImageLabels(busybox); } } @Test void standardLabelsAreAddedWhenUsingJibAddLabel() { try ( GenericContainer busybox = new GenericContainer<>( new JibImage( "busybox:1.35", jibContainerBuilder -> { return jibContainerBuilder.setEntrypoint("echo", "Hello World").addLabel("foo", "bar"); } ) ) .withStartupCheckStrategy(new OneShotStartupCheckStrategy().withTimeout(Duration.ofSeconds(3))) ) { busybox.start(); assertImageLabels(busybox); } } private static void assertImageLabels(GenericContainer busybox) { String image = busybox.getContainerInfo().getConfig().getImage(); InspectImageResponse imageResponse = DockerClientFactory.lazyClient().inspectImageCmd(image).exec(); assertThat(imageResponse.getConfig().getLabels()) .containsEntry("foo", "bar") .containsKeys( "org.testcontainers", "org.testcontainers.sessionId", "org.testcontainers.lang", "org.testcontainers.version" ); } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/MultiStageBuildTest.java ================================================ package org.testcontainers.containers; import org.junit.jupiter.api.Test; import org.testcontainers.images.builder.ImageFromDockerfile; import java.io.IOException; import java.nio.file.Paths; import static org.assertj.core.api.Assertions.assertThat; class MultiStageBuildTest { @Test void testDockerMultistageBuild() throws IOException, InterruptedException { try ( GenericContainer container = new GenericContainer<>( new ImageFromDockerfile() .withDockerfile(Paths.get("src/test/resources/Dockerfile-multistage")) .withTarget("builder") ) .withCommand("/bin/sh", "-c", "sleep 10") ) { container.start(); assertThat(container.execInContainer("pwd").getStdout()).contains("/my-files"); assertThat(container.execInContainer("ls").getStdout()).contains("hello.txt"); } } @Test void shouldBuildMultistageBuildWithBuildImageCmdModifier() throws IOException, InterruptedException { try ( GenericContainer container = new GenericContainer<>( new ImageFromDockerfile() .withDockerfile(Paths.get("src/test/resources/Dockerfile-multistage")) .withBuildImageCmdModifier(cmd -> cmd.withTarget("builder")) ) .withCommand("/bin/sh", "-c", "sleep 10") ) { container.start(); assertThat(container.execInContainer("pwd").getStdout()).contains("/my-files"); assertThat(container.execInContainer("ls").getStdout()).contains("hello.txt"); } } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/NetworkTest.java ================================================ package org.testcontainers.containers; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.testcontainers.DockerClientFactory; import org.testcontainers.TestImages; import static org.assertj.core.api.Assertions.assertThat; class NetworkTest { @Nested class WithRules { public Network network = Network.newNetwork(); public GenericContainer foo = new GenericContainer<>(TestImages.TINY_IMAGE) .withNetwork(network) .withNetworkAliases("foo") .withCommand("/bin/sh", "-c", "while true ; do printf 'HTTP/1.1 200 OK\\n\\nyay' | nc -l -p 8080; done"); public GenericContainer bar = new GenericContainer<>(TestImages.TINY_IMAGE) .withNetwork(network) .withCommand("top"); void testNetworkSupport() throws Exception { foo.start(); bar.start(); String response = bar.execInContainer("wget", "-O", "-", "http://foo:8080").getStdout(); assertThat(response).as("received response").isEqualTo("yay"); } } @Nested class WithoutRules { @Test void testNetworkSupport() throws Exception { // useCustomNetwork { try ( Network network = Network.newNetwork(); GenericContainer foo = new GenericContainer<>(TestImages.TINY_IMAGE) .withNetwork(network) .withNetworkAliases("foo") .withCommand( "/bin/sh", "-c", "while true ; do printf 'HTTP/1.1 200 OK\\n\\nyay' | nc -l -p 8080; done" ); GenericContainer bar = new GenericContainer<>(TestImages.TINY_IMAGE) .withNetwork(network) .withCommand("top") ) { foo.start(); bar.start(); String response = bar.execInContainer("wget", "-O", "-", "http://foo:8080").getStdout(); assertThat(response).as("received response").isEqualTo("yay"); } // } } @Test void testBuilder() { try (Network network = Network.builder().driver("macvlan").build()) { String id = network.getId(); assertThat( DockerClientFactory.instance().client().inspectNetworkCmd().withNetworkId(id).exec().getDriver() ) .as("Flag is set") .isEqualTo("macvlan"); } } @Test void testModifiers() { try ( Network network = Network.builder().createNetworkCmdModifier(cmd -> cmd.withDriver("macvlan")).build() ) { String id = network.getId(); assertThat( DockerClientFactory.instance().client().inspectNetworkCmd().withNetworkId(id).exec().getDriver() ) .as("Flag is set") .isEqualTo("macvlan"); } } @Test void testReusability() { try (Network network = Network.newNetwork()) { String firstId = network.getId(); assertThat(DockerClientFactory.instance().client().inspectNetworkCmd().withNetworkId(firstId).exec()) .as("Network exists") .isNotNull(); network.close(); assertThat( DockerClientFactory .instance() .client() .inspectNetworkCmd() .withNetworkId(network.getId()) .exec() .getId() ) .as("New network created") .isNotEqualTo(firstId); } } } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileBean.java ================================================ package org.testcontainers.containers; public class ParsedDockerComposeFileBean { public String foo; public ParsedDockerComposeFileBean(String foo) { this.foo = foo; } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java ================================================ package org.testcontainers.containers; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Sets; import lombok.SneakyThrows; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import java.io.File; import java.io.PrintWriter; import java.nio.file.Path; import java.util.Collections; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; class ParsedDockerComposeFileValidationTest { @TempDir public Path temporaryFolder; @Test void shouldValidate() { File file = new File("src/test/resources/docker-compose-container-name-v1.yml"); assertThatThrownBy(() -> { new ParsedDockerComposeFile(file); }) .hasMessageContaining(file.getAbsolutePath()) .hasMessageContaining("'container_name' property set for service 'redis'"); } @Test void shouldRejectContainerNameV1() { assertThatThrownBy(() -> { new ParsedDockerComposeFile(ImmutableMap.of("redis", ImmutableMap.of("container_name", "redis"))); }) .hasMessageContaining("'container_name' property set for service 'redis'"); } @Test void shouldRejectContainerNameV2() { assertThatThrownBy(() -> { new ParsedDockerComposeFile( ImmutableMap.of( "version", "2", "services", ImmutableMap.of("redis", ImmutableMap.of("container_name", "redis")) ) ); }) .hasMessageContaining("'container_name' property set for service 'redis'"); } @Test void shouldIgnoreUnknownStructure() { // Everything is a list new ParsedDockerComposeFile(Collections.emptyMap()); // services is not a map but List new ParsedDockerComposeFile(ImmutableMap.of("version", "2", "services", Collections.emptyList())); // services is not a collection new ParsedDockerComposeFile(ImmutableMap.of("version", "2", "services", true)); // no services while version is defined new ParsedDockerComposeFile(ImmutableMap.of("version", "9000")); } @Test @SneakyThrows void shouldRejectDeserializationOfArbitraryClasses() { // Reject deserialization gadget chain attacks: https://nvd.nist.gov/vuln/detail/CVE-2022-1471 // https://raw.githubusercontent.com/mbechler/marshalsec/master/marshalsec.pdf File file = new File("src/test/resources/docker-compose-deserialization.yml"); // ParsedDockerComposeFile should reject deserialization of ParsedDockerComposeFileBean assertThatThrownBy(() -> { new ParsedDockerComposeFile(file); }) .hasMessageContaining(file.getAbsolutePath()) .hasMessageContaining("Unable to parse YAML file"); } @Test void shouldObtainImageNamesV1() { File file = new File("src/test/resources/docker-compose-imagename-parsing-v1.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")) ); // redis, mysql from compose file, postgres from Dockerfile build } @Test void shouldObtainImageNamesV2() { File file = new File("src/test/resources/docker-compose-imagename-parsing-v2.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 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 void shouldObtainImageFromDockerfileBuild() { File file = new File("src/test/resources/docker-compose-imagename-parsing-dockerfile.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("alpine:3.17")) ); // r/ redis, mysql from compose file, alpine:3.17 from Dockerfile build } @Test void shouldObtainImageFromDockerfileBuildWithContext() { File file = new File("src/test/resources/docker-compose-imagename-parsing-dockerfile-with-context.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("alpine:3.17")) ); // redis, mysql from compose file, alpine:3.17 from Dockerfile build } @Test void shouldSupportALotOfAliases() throws Exception { File file = temporaryFolder.resolve("tmp-docker-compose.yml").toFile(); try (PrintWriter writer = new PrintWriter(file)) { writer.println("x-entry: &entry"); writer.println(" key: value"); writer.println(); writer.println("services:"); for (int i = 0; i < 1_000; i++) { writer.println(" service" + i + ":"); writer.println(" image: busybox"); writer.println(" environment:"); writer.println(" <<: *entry"); } } assertThatNoException().isThrownBy(() -> new ParsedDockerComposeFile(file)); } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/ReusabilityUnitTests.java ================================================ package org.testcontainers.containers; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.CreateContainerCmd; import com.github.dockerjava.api.command.CreateContainerResponse; import com.github.dockerjava.api.command.InspectContainerCmd; import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.api.command.ListContainersCmd; import com.github.dockerjava.api.command.StartContainerCmd; import com.github.dockerjava.api.model.Container; import com.github.dockerjava.core.command.CreateContainerCmdImpl; import com.github.dockerjava.core.command.InspectContainerCmdImpl; import com.github.dockerjava.core.command.ListContainersCmdImpl; import com.github.dockerjava.core.command.StartContainerCmdImpl; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedClass; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Answers; import org.mockito.Mockito; import org.mockito.stubbing.Answer; import org.testcontainers.DockerClientFactory; import org.testcontainers.TestImages; import org.testcontainers.containers.startupcheck.StartupCheckStrategy; import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy; import org.testcontainers.utility.MockTestcontainersConfigurationExtension; import org.testcontainers.utility.MountableFile; import org.testcontainers.utility.TestcontainersConfiguration; import java.io.File; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assumptions.assumeThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; class ReusabilityUnitTests { @Nested class CanBeReusedTest { public static Object[][] data() { return new Object[][] { { "generic", new GenericContainer<>(TestImages.TINY_IMAGE), true }, { "anonymous generic", new GenericContainer(TestImages.TINY_IMAGE) {}, true }, { "custom", new CustomContainer(), true }, { "anonymous custom", new CustomContainer() {}, true }, { "custom with containerIsCreated", new CustomContainerWithContainerIsCreated(), false }, }; } @ParameterizedTest(name = "{0}") @MethodSource("data") void shouldBeReusable(String name, GenericContainer container, boolean reusable) { if (reusable) { assertThat(container.canBeReused()).as("Is reusable").isTrue(); } else { assertThat(container.canBeReused()).as("Is not reusable").isFalse(); } } static class CustomContainer extends GenericContainer { CustomContainer() { super(TestImages.TINY_IMAGE); } } static class CustomContainerWithContainerIsCreated extends GenericContainer { CustomContainerWithContainerIsCreated() { super(TestImages.TINY_IMAGE); } @Override protected void containerIsCreated(String containerId) { super.containerIsCreated(containerId); } } } @Nested class HooksTest extends AbstractReusabilityTest { List script = new ArrayList<>(); GenericContainer container = makeReusable( new GenericContainer(TestImages.TINY_IMAGE) { @Override protected boolean canBeReused() { // Because we override "containerIsCreated" return true; } @Override protected void containerIsCreated(String containerId) { script.add("containerIsCreated"); } @Override protected void containerIsStarting(InspectContainerResponse containerInfo, boolean reused) { script.add("containerIsStarting(reused=" + reused + ")"); } @Override protected void containerIsStarted(InspectContainerResponse containerInfo, boolean reused) { script.add("containerIsStarted(reused=" + reused + ")"); } } ); @Test void shouldSetLabelsIfEnvironmentDoesNotSupportReuse() { Mockito.doReturn(false).when(TestcontainersConfiguration.getInstance()).environmentSupportsReuse(); String containerId = randomContainerId(); when(client.createContainerCmd(any())).then(createContainerAnswer(containerId)); when(client.listContainersCmd()).then(listContainersAnswer()); when(client.startContainerCmd(containerId)).then(startContainerAnswer()); when(client.inspectContainerCmd(containerId)).then(inspectContainerAnswer()); container.start(); assertThat(script) .containsExactly( "containerIsCreated", "containerIsStarting(reused=false)", "containerIsStarted(reused=false)" ); } @Test void shouldCallHookIfReused() { Mockito.doReturn(true).when(TestcontainersConfiguration.getInstance()).environmentSupportsReuse(); String containerId = randomContainerId(); when(client.createContainerCmd(any())).then(createContainerAnswer(containerId)); String existingContainerId = randomContainerId(); when(client.listContainersCmd()).then(listContainersAnswer(existingContainerId)); when(client.inspectContainerCmd(existingContainerId)).then(inspectContainerAnswer()); container.start(); assertThat(script).containsExactly("containerIsStarting(reused=true)", "containerIsStarted(reused=true)"); } @Test void shouldNotCallHookIfNotReused() { String containerId = randomContainerId(); when(client.createContainerCmd(any())).then(createContainerAnswer(containerId)); when(client.listContainersCmd()).then(listContainersAnswer()); when(client.startContainerCmd(containerId)).then(startContainerAnswer()); when(client.inspectContainerCmd(containerId)).then(inspectContainerAnswer()); container.start(); assertThat(script) .containsExactly( "containerIsCreated", "containerIsStarting(reused=false)", "containerIsStarted(reused=false)" ); } } @Nested class HashTest extends AbstractReusabilityTest { protected GenericContainer container = makeReusable( new GenericContainer(TestImages.TINY_IMAGE) { @Override public void copyFileToContainer(MountableFile mountableFile, String containerPath) { // NOOP } } ); @Test void shouldStartIfListReturnsEmpty() { String containerId = randomContainerId(); when(client.createContainerCmd(any())).then(createContainerAnswer(containerId)); when(client.listContainersCmd()).then(listContainersAnswer()); when(client.startContainerCmd(containerId)).then(startContainerAnswer()); when(client.inspectContainerCmd(containerId)).then(inspectContainerAnswer()); container.start(); Mockito.verify(client, Mockito.atLeastOnce()).startContainerCmd(containerId); } @Test void shouldReuseIfListReturnsID() { Mockito.doReturn(true).when(TestcontainersConfiguration.getInstance()).environmentSupportsReuse(); String containerId = randomContainerId(); when(client.createContainerCmd(any())).then(createContainerAnswer(containerId)); String existingContainerId = randomContainerId(); when(client.listContainersCmd()).then(listContainersAnswer(existingContainerId)); when(client.inspectContainerCmd(existingContainerId)).then(inspectContainerAnswer()); container.start(); Mockito.verify(client, Mockito.never()).startContainerCmd(containerId); Mockito.verify(client, Mockito.never()).startContainerCmd(existingContainerId); } @Test void shouldSetLabelsIfEnvironmentDoesNotSupportReuse() { Mockito.doReturn(false).when(TestcontainersConfiguration.getInstance()).environmentSupportsReuse(); AtomicReference commandRef = new AtomicReference<>(); String containerId = randomContainerId(); when(client.createContainerCmd(any())).then(createContainerAnswer(containerId, commandRef::set)); when(client.startContainerCmd(containerId)).then(startContainerAnswer()); when(client.inspectContainerCmd(containerId)).then(inspectContainerAnswer()); container.start(); assertThat(commandRef) .isNotNull() .satisfies(command -> { assertThat(command.get().getLabels()) .containsKeys(DockerClientFactory.TESTCONTAINERS_SESSION_ID_LABEL); }); } @Test void shouldSetCopiedFilesHashLabel() { Mockito.doReturn(true).when(TestcontainersConfiguration.getInstance()).environmentSupportsReuse(); AtomicReference commandRef = new AtomicReference<>(); String containerId = randomContainerId(); when(client.createContainerCmd(any())).then(createContainerAnswer(containerId, commandRef::set)); when(client.listContainersCmd()).then(listContainersAnswer()); when(client.startContainerCmd(containerId)).then(startContainerAnswer()); when(client.inspectContainerCmd(containerId)).then(inspectContainerAnswer()); container.start(); assertThat(commandRef).isNotNull(); assertThat(commandRef.get().getLabels()).containsKeys(GenericContainer.COPIED_FILES_HASH_LABEL); } @Test void shouldHashCopiedFiles() { Mockito.doReturn(true).when(TestcontainersConfiguration.getInstance()).environmentSupportsReuse(); AtomicReference commandRef = new AtomicReference<>(); String containerId = randomContainerId(); when(client.createContainerCmd(any())).then(createContainerAnswer(containerId, commandRef::set)); when(client.listContainersCmd()).then(listContainersAnswer()); when(client.startContainerCmd(containerId)).then(startContainerAnswer()); when(client.inspectContainerCmd(containerId)).then(inspectContainerAnswer()); container.start(); assertThat(commandRef).isNotNull(); Map labels = commandRef.get().getLabels(); assertThat(labels).containsKeys(GenericContainer.COPIED_FILES_HASH_LABEL); String oldHash = labels.get(GenericContainer.COPIED_FILES_HASH_LABEL); // Simulate stop container.containerId = null; container.withCopyFileToContainer( MountableFile.forClasspathResource("test_copy_to_container.txt"), "/foo/bar" ); container.start(); assertThat(commandRef.get().getLabels()) .hasEntrySatisfying( GenericContainer.COPIED_FILES_HASH_LABEL, newHash -> { assertThat(newHash).as("new hash").isNotEqualTo(oldHash); } ); } } @Nested @ParameterizedClass @MethodSource("strategies") class CopyFilesHashTest { private final TestStrategy strategy; interface TestStrategy { void withCopyFileToContainer(MountableFile mountableFile, String path); void clear(); } private static class MountableFileTestStrategy implements TestStrategy { private final GenericContainer container; private MountableFileTestStrategy(GenericContainer container) { this.container = container; } @Override public void withCopyFileToContainer(MountableFile mountableFile, String path) { container.withCopyFileToContainer(mountableFile, path); } @Override public void clear() { container.getCopyToFileContainerPathMap().clear(); } } private static class TransferableTestStrategy implements TestStrategy { private final GenericContainer container; private TransferableTestStrategy(GenericContainer container) { this.container = container; } @Override public void withCopyFileToContainer(MountableFile mountableFile, String path) { container.withCopyToContainer(mountableFile, path); } @Override public void clear() { container.getCopyToTransferableContainerPathMap().clear(); } } public static List, TestStrategy>> strategies() { return Arrays.asList(MountableFileTestStrategy::new, TransferableTestStrategy::new); } GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE); public CopyFilesHashTest(Function, TestStrategy> strategyFactory) { this.strategy = strategyFactory.apply(container); } @Test void empty() { assertThat(container.hashCopiedFiles()).isNotNull(); } @Test void oneFile() { long emptyHash = container.hashCopiedFiles().getValue(); strategy.withCopyFileToContainer( MountableFile.forClasspathResource("test_copy_to_container.txt"), "/foo/bar" ); assertThat(container.hashCopiedFiles().getValue()).isNotEqualTo(emptyHash); } @Test void differentPath() { MountableFile mountableFile = MountableFile.forClasspathResource("test_copy_to_container.txt"); strategy.withCopyFileToContainer(mountableFile, "/foo/bar"); long hash1 = container.hashCopiedFiles().getValue(); strategy.clear(); strategy.withCopyFileToContainer(mountableFile, "/foo/baz"); assertThat(container.hashCopiedFiles().getValue()).isNotEqualTo(hash1); } @Test void detectsChangesInFile() throws Exception { Path path = File.createTempFile("reusable_test", ".txt").toPath(); MountableFile mountableFile = MountableFile.forHostPath(path); strategy.withCopyFileToContainer(mountableFile, "/foo/bar"); long hash1 = container.hashCopiedFiles().getValue(); Files.write(path, UUID.randomUUID().toString().getBytes()); assertThat(container.hashCopiedFiles().getValue()).isNotEqualTo(hash1); } @Test void multipleFiles() { strategy.withCopyFileToContainer( MountableFile.forClasspathResource("test_copy_to_container.txt"), "/foo/bar" ); long hash1 = container.hashCopiedFiles().getValue(); strategy.withCopyFileToContainer( MountableFile.forClasspathResource("mappable-resource/test-resource.txt"), "/foo/baz" ); assertThat(container.hashCopiedFiles().getValue()).isNotEqualTo(hash1); } @Test void folder() throws Exception { long emptyHash = container.hashCopiedFiles().getValue(); Path tempDirectory = Files.createTempDirectory("reusable_test"); MountableFile mountableFile = MountableFile.forHostPath(tempDirectory); strategy.withCopyFileToContainer(mountableFile, "/foo/bar/"); assertThat(container.hashCopiedFiles().getValue()).isNotEqualTo(emptyHash); } @Test void changesInFolder() throws Exception { Path tempDirectory = Files.createTempDirectory("reusable_test"); MountableFile mountableFile = MountableFile.forHostPath(tempDirectory); assertThat(new File(mountableFile.getResolvedPath())).isDirectory(); strategy.withCopyFileToContainer(mountableFile, "/foo/bar/"); long hash1 = container.hashCopiedFiles().getValue(); Path fileInFolder = Files.createFile( // Create file in the sub-folder Files.createDirectory(tempDirectory.resolve("sub")).resolve("test.txt") ); assertThat(fileInFolder).exists(); Files.write(fileInFolder, UUID.randomUUID().toString().getBytes()); assertThat(container.hashCopiedFiles().getValue()).isNotEqualTo(hash1); } @Test void folderAndFile() throws Exception { Path tempDirectory = Files.createTempDirectory("reusable_test"); MountableFile mountableFile = MountableFile.forHostPath(tempDirectory); assertThat(new File(mountableFile.getResolvedPath())).isDirectory(); strategy.withCopyFileToContainer(mountableFile, "/foo/bar/"); long hash1 = container.hashCopiedFiles().getValue(); strategy.withCopyFileToContainer( MountableFile.forClasspathResource("test_copy_to_container.txt"), "/foo/baz" ); assertThat(container.hashCopiedFiles().getValue()).isNotEqualTo(hash1); } @Test void filePermissions() throws Exception { Path path = File.createTempFile("reusable_test", ".txt").toPath(); path.toFile().setExecutable(false); MountableFile mountableFile = MountableFile.forHostPath(path); strategy.withCopyFileToContainer(mountableFile, "/foo/bar"); long hash1 = container.hashCopiedFiles().getValue(); assumeThat(path.toFile().canExecute()).isFalse(); path.toFile().setExecutable(true); assertThat(container.hashCopiedFiles().getValue()).isNotEqualTo(hash1); } @Test void folderPermissions() throws Exception { Path tempDirectory = Files.createTempDirectory("reusable_test"); MountableFile mountableFile = MountableFile.forHostPath(tempDirectory); assertThat(new File(mountableFile.getResolvedPath())).isDirectory(); Path subDir = Files.createDirectory(tempDirectory.resolve("sub")); subDir.toFile().setWritable(false); assumeThat(subDir.toFile().canWrite()).isFalse(); strategy.withCopyFileToContainer(mountableFile, "/foo/bar/"); long hash1 = container.hashCopiedFiles().getValue(); subDir.toFile().setWritable(true); assumeThat(subDir.toFile()).canWrite(); assertThat(container.hashCopiedFiles().getValue()).isNotEqualTo(hash1); } } @ExtendWith(MockTestcontainersConfigurationExtension.class) public abstract static class AbstractReusabilityTest { protected DockerClient client = Mockito.mock(DockerClient.class); protected > T makeReusable(T container) { container.dockerClient = client; container.withNetworkMode("none"); // to disable the port forwarding container.withStartupCheckStrategy( new StartupCheckStrategy() { @Override public boolean waitUntilStartupSuccessful(DockerClient dockerClient, String containerId) { // Skip DockerClient rate limiter return true; } @Override public StartupStatus checkStartupState(DockerClient dockerClient, String containerId) { return StartupStatus.SUCCESSFUL; } } ); container.waitingFor( new AbstractWaitStrategy() { @Override protected void waitUntilReady() {} } ); container.withReuse(true); return container; } protected String randomContainerId() { return UUID.randomUUID().toString(); } protected Answer listContainersAnswer(String... ids) { return invocation -> { ListContainersCmd.Exec exec = command -> { return new ObjectMapper() .convertValue( Stream.of(ids).map(id -> Collections.singletonMap("Id", id)).collect(Collectors.toList()), new TypeReference>() {} ); }; return new ListContainersCmdImpl(exec); }; } protected Answer createContainerAnswer(String containerId) { return createContainerAnswer(containerId, command -> {}); } protected Answer createContainerAnswer( String containerId, Consumer cmdConsumer ) { return invocation -> { CreateContainerCmd.Exec exec = command -> { cmdConsumer.accept(command); CreateContainerResponse response = new CreateContainerResponse(); response.setId(containerId); return response; }; return new CreateContainerCmdImpl(exec, null, "image:latest"); }; } protected Answer startContainerAnswer() { return invocation -> { StartContainerCmd.Exec exec = command -> { return null; }; return new StartContainerCmdImpl(exec, invocation.getArgument(0)); }; } protected Answer inspectContainerAnswer() { return invocation -> { InspectContainerCmd.Exec exec = command -> { InspectContainerResponse stubResponse = Mockito.mock( InspectContainerResponse.class, Answers.RETURNS_DEEP_STUBS ); when(stubResponse.getNetworkSettings().getPorts().getBindings()).thenReturn(Collections.emptyMap()); return stubResponse; }; return new InspectContainerCmdImpl(exec, invocation.getArgument(0)); }; } } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/output/ContainerLogsTest.java ================================================ package org.testcontainers.containers.output; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.testcontainers.TestImages; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; import static org.assertj.core.api.Assertions.assertThat; class ContainerLogsTest { @Test @Disabled("fails due to the timing of the shell's decision to flush") void getLogsReturnsAllLogsToDate() { try (GenericContainer container = shortLivedContainer()) { container.start(); final String logs = container.getLogs(); assertThat(logs).as("stdout and stderr are reflected in the returned logs").isEqualTo("stdout\nstderr"); } } @Test void getLogsContainsBothOutputTypes() { try (GenericContainer container = shortLivedContainer()) { container.start(); // docsGetAllLogs { final String logs = container.getLogs(); // } assertThat(logs).as("stdout is reflected in the returned logs").contains("stdout"); assertThat(logs).as("stderr is reflected in the returned logs").contains("stderr"); } } @Test void getLogsReturnsStdOutToDate() { try (GenericContainer container = shortLivedContainer()) { container.start(); // docsGetStdOut { final String logs = container.getLogs(OutputFrame.OutputType.STDOUT); // } assertThat(logs).as("stdout is reflected in the returned logs").contains("stdout"); } } @Test void getLogsReturnsStdErrToDate() { try (GenericContainer container = shortLivedContainer()) { container.start(); // docsGetStdErr { final String logs = container.getLogs(OutputFrame.OutputType.STDERR); // } assertThat(logs).as("stderr is reflected in the returned logs").contains("stderr"); } } @Test void getLogsForLongRunningContainer() throws InterruptedException { try (GenericContainer container = longRunningContainer()) { container.start(); Thread.sleep(1000L); final String logs = container.getLogs(OutputFrame.OutputType.STDOUT); assertThat(logs).as("stdout is reflected in the returned logs for a running container").contains("seq=0"); } } private static GenericContainer shortLivedContainer() { return new GenericContainer<>(TestImages.ALPINE_IMAGE) .withCommand("/bin/sh", "-c", "echo -n 'stdout' && echo -n 'stderr' 1>&2") .withStartupCheckStrategy(new OneShotStartupCheckStrategy()); } private static GenericContainer longRunningContainer() { return new GenericContainer<>(TestImages.ALPINE_IMAGE).withCommand("ping -c 100 127.0.0.1"); } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/output/FrameConsumerResultCallbackTest.java ================================================ package org.testcontainers.containers.output; import com.github.dockerjava.api.model.Frame; import com.github.dockerjava.api.model.StreamType; import org.junit.jupiter.api.Test; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import static org.assertj.core.api.Assertions.assertThat; class FrameConsumerResultCallbackTest { private static final String FRAME_PAYLOAD = "\u001B[0;32mТест1\u001B[0m\n\u001B[1;33mTest2\u001B[0m\n\u001B[0;31mTest3\u001B[0m"; private static final String LOG_RESULT = "Тест1\nTest2\nTest3"; @Test void passStderrFrameWithoutColors() throws IOException { FrameConsumerResultCallback callback = new FrameConsumerResultCallback(); ToStringConsumer consumer = new ToStringConsumer(); callback.addConsumer(OutputFrame.OutputType.STDERR, consumer); callback.onNext(new Frame(StreamType.STDERR, FRAME_PAYLOAD.getBytes())); callback.close(); assertThat(consumer.toUtf8String()).isEqualTo(LOG_RESULT); } @Test void passStderrFrameWithColors() throws IOException { FrameConsumerResultCallback callback = new FrameConsumerResultCallback(); ToStringConsumer consumer = new ToStringConsumer().withRemoveAnsiCodes(false); callback.addConsumer(OutputFrame.OutputType.STDERR, consumer); callback.onNext(new Frame(StreamType.STDERR, FRAME_PAYLOAD.getBytes())); callback.close(); assertThat(consumer.toUtf8String()).isEqualTo(FRAME_PAYLOAD); } @Test void passStdoutFrameWithoutColors() throws IOException { FrameConsumerResultCallback callback = new FrameConsumerResultCallback(); ToStringConsumer consumer = new ToStringConsumer(); callback.addConsumer(OutputFrame.OutputType.STDOUT, consumer); callback.onNext(new Frame(StreamType.STDOUT, FRAME_PAYLOAD.getBytes())); callback.close(); assertThat(consumer.toUtf8String()).isEqualTo(LOG_RESULT); } @Test void passStdoutFrameWithColors() throws IOException { FrameConsumerResultCallback callback = new FrameConsumerResultCallback(); ToStringConsumer consumer = new ToStringConsumer().withRemoveAnsiCodes(false); callback.addConsumer(OutputFrame.OutputType.STDOUT, consumer); callback.onNext(new Frame(StreamType.STDOUT, FRAME_PAYLOAD.getBytes())); callback.close(); assertThat(consumer.toUtf8String()).isEqualTo(FRAME_PAYLOAD); } @Test void basicConsumer() throws IOException { FrameConsumerResultCallback callback = new FrameConsumerResultCallback(); BasicConsumer consumer = new BasicConsumer(); callback.addConsumer(OutputFrame.OutputType.STDOUT, consumer); callback.onNext(new Frame(StreamType.STDOUT, FRAME_PAYLOAD.getBytes())); callback.close(); assertThat(consumer.toString()).isEqualTo(LOG_RESULT); } @Test void passStdoutNull() throws IOException { FrameConsumerResultCallback callback = new FrameConsumerResultCallback(); ToStringConsumer consumer = new ToStringConsumer().withRemoveAnsiCodes(false); callback.addConsumer(OutputFrame.OutputType.STDOUT, consumer); callback.onNext(new Frame(StreamType.STDOUT, null)); callback.close(); assertThat(consumer.toUtf8String()).isEqualTo(""); } @Test void passStdoutEmptyLine() throws IOException { String payload = ""; FrameConsumerResultCallback callback = new FrameConsumerResultCallback(); ToStringConsumer consumer = new ToStringConsumer().withRemoveAnsiCodes(false); callback.addConsumer(OutputFrame.OutputType.STDOUT, consumer); callback.onNext(new Frame(StreamType.STDOUT, payload.getBytes())); callback.close(); assertThat(consumer.toUtf8String()).isEqualTo(payload); } @Test void passStdoutSingleLine() throws IOException { String payload = "Test"; FrameConsumerResultCallback callback = new FrameConsumerResultCallback(); ToStringConsumer consumer = new ToStringConsumer().withRemoveAnsiCodes(false); callback.addConsumer(OutputFrame.OutputType.STDOUT, consumer); callback.onNext(new Frame(StreamType.STDOUT, payload.getBytes())); callback.close(); assertThat(consumer.toUtf8String()).isEqualTo(payload); } @Test void passStdoutSingleLineWithNewline() throws IOException { String payload = "Test\n"; FrameConsumerResultCallback callback = new FrameConsumerResultCallback(); ToStringConsumer consumer = new ToStringConsumer().withRemoveAnsiCodes(false); callback.addConsumer(OutputFrame.OutputType.STDOUT, consumer); callback.onNext(new Frame(StreamType.STDOUT, payload.getBytes())); callback.close(); assertThat(consumer.toUtf8String()).isEqualTo(payload); } @Test void passRawFrameWithoutColors() throws TimeoutException, IOException { FrameConsumerResultCallback callback = new FrameConsumerResultCallback(); WaitingConsumer waitConsumer = new WaitingConsumer(); callback.addConsumer(OutputFrame.OutputType.STDOUT, waitConsumer); callback.onNext(new Frame(StreamType.RAW, FRAME_PAYLOAD.getBytes())); waitConsumer.waitUntil( frame -> frame.getType() == OutputFrame.OutputType.STDOUT && frame.getUtf8String().equals("Test2\n"), 1, TimeUnit.SECONDS ); waitConsumer.waitUntil( frame -> frame.getType() == OutputFrame.OutputType.STDOUT && frame.getUtf8String().equals("Тест1\n"), 1, TimeUnit.SECONDS ); Exception exception = null; try { waitConsumer.waitUntil( frame -> frame.getType() == OutputFrame.OutputType.STDOUT && frame.getUtf8String().equals("Test3"), 1, TimeUnit.SECONDS ); } catch (Exception e) { exception = e; } assertThat(exception instanceof TimeoutException).isTrue(); callback.close(); waitConsumer.waitUntil( frame -> frame.getType() == OutputFrame.OutputType.STDOUT && frame.getUtf8String().equals("Test3"), 1, TimeUnit.SECONDS ); } @Test void passRawFrameWithColors() throws TimeoutException, IOException { FrameConsumerResultCallback callback = new FrameConsumerResultCallback(); WaitingConsumer waitConsumer = new WaitingConsumer().withRemoveAnsiCodes(false); callback.addConsumer(OutputFrame.OutputType.STDOUT, waitConsumer); callback.onNext(new Frame(StreamType.RAW, FRAME_PAYLOAD.getBytes())); waitConsumer.waitUntil( frame -> { return ( frame.getType() == OutputFrame.OutputType.STDOUT && frame.getUtf8String().equals("\u001B[1;33mTest2\u001B[0m\n") ); }, 1, TimeUnit.SECONDS ); waitConsumer.waitUntil( frame -> { return ( frame.getType() == OutputFrame.OutputType.STDOUT && frame.getUtf8String().equals("\u001B[0;32mТест1\u001B[0m\n") ); }, 1, TimeUnit.SECONDS ); Exception exception = null; try { waitConsumer.waitUntil( frame -> { return ( frame.getType() == OutputFrame.OutputType.STDOUT && frame.getUtf8String().equals("\u001B[0;31mTest3\u001B[0m") ); }, 1, TimeUnit.SECONDS ); } catch (Exception e) { exception = e; } assertThat(exception instanceof TimeoutException).isTrue(); callback.close(); waitConsumer.waitUntil( frame -> { return ( frame.getType() == OutputFrame.OutputType.STDOUT && frame.getUtf8String().equals("\u001B[0;31mTest3\u001B[0m") ); }, 1, TimeUnit.SECONDS ); } @Test void reconstructBreakedUnicode() throws IOException { String payload = "Тест"; byte[] payloadBytes = payload.getBytes(StandardCharsets.UTF_8); byte[] bytes1 = new byte[(int) (payloadBytes.length * 0.6)]; byte[] bytes2 = new byte[payloadBytes.length - bytes1.length]; System.arraycopy(payloadBytes, 0, bytes1, 0, bytes1.length); System.arraycopy(payloadBytes, bytes1.length, bytes2, 0, bytes2.length); FrameConsumerResultCallback callback = new FrameConsumerResultCallback(); ToStringConsumer consumer = new ToStringConsumer().withRemoveAnsiCodes(false); callback.addConsumer(OutputFrame.OutputType.STDOUT, consumer); callback.onNext(new Frame(StreamType.RAW, bytes1)); callback.onNext(new Frame(StreamType.RAW, bytes2)); callback.close(); assertThat(consumer.toUtf8String()).isEqualTo(payload); } private static class BasicConsumer implements Consumer { private StringBuilder input = new StringBuilder(); @Override public void accept(OutputFrame outputFrame) { input.append(outputFrame.getUtf8String()); } @Override public String toString() { return input.toString(); } } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/output/ToStringConsumerTest.java ================================================ package org.testcontainers.containers.output; import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.testcontainers.containers.Container.ExecResult; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; import static org.assertj.core.api.Assertions.assertThat; class ToStringConsumerTest { private static final String LARGE_PAYLOAD; static { StringBuilder builder = new StringBuilder(10_003 * 10); for (int i = 0; i < 10; i++) { builder.append(' ').append(i).append(RandomStringUtils.randomAlphabetic(10000)); } LARGE_PAYLOAD = builder.toString(); assertThat(LARGE_PAYLOAD).doesNotContain("\n"); } @Test void newlines_are_not_added_to_exec_output() throws Exception { try (GenericContainer container = new GenericContainer<>("alpine:3.17")) { container.withCommand("sleep", "2m"); container.start(); ExecResult build = container.execInContainer("echo", "-n", LARGE_PAYLOAD); assertThat(build.getStdout()).doesNotContain("\n").isEqualTo(LARGE_PAYLOAD); } } @Test @Timeout(60) void newlines_are_not_added_to_exec_output_with_tty() throws Exception { try (GenericContainer container = new GenericContainer<>("alpine:3.17")) { container.withCreateContainerCmdModifier(cmd -> { cmd.withAttachStdin(true).withStdinOpen(true).withTty(true); }); container.withCommand("sleep", "2m"); container.start(); ExecResult build = container.execInContainer("echo", "-n", LARGE_PAYLOAD); assertThat(build.getStdout()).isEqualTo(LARGE_PAYLOAD).doesNotContain("\n"); } } @Test void newlines_are_not_added_to_container_output() { try (GenericContainer container = new GenericContainer<>("alpine:3.17")) { container.withCommand("echo", "-n", LARGE_PAYLOAD); container.setStartupCheckStrategy(new OneShotStartupCheckStrategy()); container.start(); container.getDockerClient().waitContainerCmd(container.getContainerId()).start().awaitStatusCode(); assertThat(container.getLogs()).isEqualTo(LARGE_PAYLOAD).doesNotContain("\n"); } } @Test void newlines_are_not_added_to_container_output_with_tty() { try (GenericContainer container = new GenericContainer<>("alpine:3.17")) { container.withCreateContainerCmdModifier(cmd -> { cmd.withTty(true); }); container.withCommand("echo", "-n", LARGE_PAYLOAD); container.setStartupCheckStrategy(new OneShotStartupCheckStrategy()); container.start(); container.getDockerClient().waitContainerCmd(container.getContainerId()).start().awaitStatusCode(); assertThat(container.getLogs()).isEqualTo(LARGE_PAYLOAD).doesNotContain("\n"); } } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/startupcheck/IsRunningStartupCheckStrategyTest.java ================================================ package org.testcontainers.containers.startupcheck; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.testcontainers.TestImages; import org.testcontainers.containers.GenericContainer; import static org.assertj.core.api.Assertions.assertThatThrownBy; class IsRunningStartupCheckStrategyTest { @Test void testCommandQuickExitSuccess() { try (GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE).withCommand("/bin/true")) { container.start(); // should start with no Exception } } @Test @Disabled("This test can fail to throw an AssertionError if the container doesn't fail quickly enough") void testCommandQuickExitFailure() { try (GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE).withCommand("/bin/false")) { assertThatThrownBy(container::start) .hasStackTraceContaining("Container startup failed") .hasStackTraceContaining("Container did not start correctly"); } } @Test void testCommandStaysRunning() { try ( GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE).withCommand("/bin/sleep", "60") ) { container.start(); // should start with no Exception } } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/wait/internal/ExternalPortListeningCheckTest.java ================================================ package org.testcontainers.containers.wait.internal; import com.google.common.collect.ImmutableSet; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.testcontainers.containers.wait.strategy.WaitStrategyTarget; import java.net.ServerSocket; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class ExternalPortListeningCheckTest { private ServerSocket listeningSocket1; private ServerSocket listeningSocket2; private ServerSocket nonListeningSocket; private WaitStrategyTarget mockContainer; @BeforeEach public void setUp() throws Exception { listeningSocket1 = new ServerSocket(0); listeningSocket2 = new ServerSocket(0); nonListeningSocket = new ServerSocket(0); nonListeningSocket.close(); mockContainer = mock(WaitStrategyTarget.class); when(mockContainer.getHost()).thenReturn("127.0.0.1"); } @Test void singleListening() { final ExternalPortListeningCheck check = new ExternalPortListeningCheck( mockContainer, ImmutableSet.of(listeningSocket1.getLocalPort()) ); final Boolean result = check.call(); assertThat(result).as("ExternalPortListeningCheck identifies a single listening port").isTrue(); } @Test void multipleListening() { final ExternalPortListeningCheck check = new ExternalPortListeningCheck( mockContainer, ImmutableSet.of(listeningSocket1.getLocalPort(), listeningSocket2.getLocalPort()) ); final Boolean result = check.call(); assertThat(result).as("ExternalPortListeningCheck identifies multiple listening port").isTrue(); } @Test void oneNotListening() { final ExternalPortListeningCheck check = new ExternalPortListeningCheck( mockContainer, ImmutableSet.of(listeningSocket1.getLocalPort(), nonListeningSocket.getLocalPort()) ); assertThat(catchThrowable(check::call)) .as("ExternalPortListeningCheck detects a non-listening port among many") .isInstanceOf(IllegalStateException.class); } @AfterEach public void tearDown() throws Exception { listeningSocket1.close(); listeningSocket2.close(); } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/wait/internal/InternalCommandPortListeningCheckTest.java ================================================ package org.testcontainers.containers.wait.internal; import com.google.common.collect.ImmutableSet; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedClass; import org.junit.jupiter.params.provider.MethodSource; import org.rnorth.ducttape.TimeoutException; import org.rnorth.ducttape.unreliables.Unreliables; import org.testcontainers.containers.GenericContainer; import org.testcontainers.images.builder.ImageFromDockerfile; import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.fail; @ParameterizedClass(name = "{index} - {0}") @MethodSource("data") class InternalCommandPortListeningCheckTest { public static List data() { return Arrays.asList( "internal-port-check-dockerfile/Dockerfile-tcp", "internal-port-check-dockerfile/Dockerfile-nc", "internal-port-check-dockerfile/Dockerfile-bash" ); } public GenericContainer container; public InternalCommandPortListeningCheckTest(String dockerfile) { container = new GenericContainer<>( new ImageFromDockerfile() .withFileFromClasspath("Dockerfile", dockerfile) .withFileFromClasspath("nginx.conf", "internal-port-check-dockerfile/nginx.conf") ); container.start(); } @Test void singleListening() { final InternalCommandPortListeningCheck check = new InternalCommandPortListeningCheck( container, ImmutableSet.of(8080) ); Unreliables.retryUntilTrue(5, TimeUnit.SECONDS, check); } @Test void nonListening() { final InternalCommandPortListeningCheck check = new InternalCommandPortListeningCheck( container, ImmutableSet.of(8080, 1234) ); try { Unreliables.retryUntilTrue(5, TimeUnit.SECONDS, check); fail("expected to fail"); } catch (TimeoutException e) {} } @Test void lowAndHighPortListening() { final InternalCommandPortListeningCheck check = new InternalCommandPortListeningCheck( container, ImmutableSet.of(100, 8080) ); Unreliables.retryUntilTrue(5, TimeUnit.SECONDS, check); } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategyTest.java ================================================ package org.testcontainers.containers.wait.strategy; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.containers.GenericContainer; import org.testcontainers.images.builder.ImageFromDockerfile; import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; class DockerHealthcheckWaitStrategyTest { private GenericContainer container; @BeforeEach public void setUp() { // Using a Dockerfile here, since Dockerfile builder DSL doesn't support HEALTHCHECK container = new GenericContainer( new ImageFromDockerfile() .withFileFromClasspath( "write_file_and_loop.sh", "health-wait-strategy-dockerfile/write_file_and_loop.sh" ) .withFileFromClasspath("Dockerfile", "health-wait-strategy-dockerfile/Dockerfile") ) .waitingFor(Wait.forHealthcheck().withStartupTimeout(Duration.ofSeconds(3))); } @Test void startsOnceHealthy() { container.start(); } @Test void containerStartFailsIfContainerIsUnhealthy() { container.withCommand("tail", "-f", "/dev/null"); assertThat(catchThrowable(container::start)) .as("Container launch fails when unhealthy") .isInstanceOf(ContainerLaunchException.class); } } ================================================ FILE: core/src/test/java/org/testcontainers/containers/wait/strategy/WaitAllStrategyTest.java ================================================ package org.testcontainers.containers.wait.strategy; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.rnorth.ducttape.TimeoutException; import org.testcontainers.containers.GenericContainer; import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; class WaitAllStrategyTest { @Mock private GenericContainer container; @Mock private WaitStrategy strategy1; @Mock private WaitStrategy strategy2; @Mock private WaitStrategy strategy3; @BeforeEach public void setUp() { MockitoAnnotations.initMocks(this); } /* * Dummy-based tests, to check that timeout values are propagated correctly, without involving actual timing-sensitive code */ @Test void parentTimeoutApplies() { DummyStrategy child1 = new DummyStrategy(Duration.ofMillis(10)); child1.withStartupTimeout(Duration.ofMillis(20)); assertThat(child1.startupTimeout.toMillis()).as("withStartupTimeout directly sets the timeout").isEqualTo(20L); new WaitAllStrategy().withStrategy(child1).withStartupTimeout(Duration.ofMillis(30)); assertThat(child1.startupTimeout.toMillis()).as("WaitAllStrategy overrides a child's timeout").isEqualTo(30L); } @Test void parentTimeoutAppliesToMultipleChildren() { Duration defaultInnerWait = Duration.ofMillis(2); Duration outerWait = Duration.ofMillis(6); DummyStrategy child1 = new DummyStrategy(defaultInnerWait); DummyStrategy child2 = new DummyStrategy(defaultInnerWait); new WaitAllStrategy().withStrategy(child1).withStrategy(child2).withStartupTimeout(outerWait); assertThat(child1.startupTimeout.toMillis()) .as("WaitAllStrategy overrides a child's timeout (1st)") .isEqualTo(6L); assertThat(child2.startupTimeout.toMillis()) .as("WaitAllStrategy overrides a child's timeout (2nd)") .isEqualTo(6L); } @Test void parentTimeoutAppliesToAdditionalChildren() { Duration defaultInnerWait = Duration.ofMillis(2); Duration outerWait = Duration.ofMillis(20); DummyStrategy child1 = new DummyStrategy(defaultInnerWait); DummyStrategy child2 = new DummyStrategy(defaultInnerWait); new WaitAllStrategy().withStrategy(child1).withStartupTimeout(outerWait).withStrategy(child2); assertThat(child1.startupTimeout.toMillis()) .as("WaitAllStrategy overrides a child's timeout (1st)") .isEqualTo(20L); assertThat(child2.startupTimeout.toMillis()) .as("WaitAllStrategy overrides a child's timeout (2nd, additional)") .isEqualTo(20L); } /* * Mock-based tests to check overall behaviour, without involving timing-sensitive code */ @Test void childExecutionTest() { final WaitStrategy underTest = new WaitAllStrategy().withStrategy(strategy1).withStrategy(strategy2); doNothing().when(strategy1).waitUntilReady(eq(container)); doNothing().when(strategy2).waitUntilReady(eq(container)); underTest.waitUntilReady(container); InOrder inOrder = inOrder(strategy1, strategy2); inOrder.verify(strategy1).waitUntilReady(any()); inOrder.verify(strategy2).waitUntilReady(any()); } @Test void withoutOuterTimeoutShouldRelyOnInnerStrategies() { final WaitStrategy underTest = new WaitAllStrategy(WaitAllStrategy.Mode.WITH_INDIVIDUAL_TIMEOUTS_ONLY) .withStrategy(strategy1) .withStrategy(strategy2) .withStrategy(strategy3); doNothing().when(strategy1).waitUntilReady(eq(container)); doThrow(TimeoutException.class).when(strategy2).waitUntilReady(eq(container)); assertThat( catchThrowable(() -> { underTest.waitUntilReady(container); }) ) .as("The outer strategy timeout applies") .isInstanceOf(TimeoutException.class); InOrder inOrder = inOrder(strategy1, strategy2, strategy3); inOrder.verify(strategy1).waitUntilReady(any()); inOrder.verify(strategy2).waitUntilReady(any()); inOrder.verify(strategy3, never()).waitUntilReady(any()); } @Test void timeoutChangeShouldNotBePossibleWithIndividualTimeoutMode() { final WaitStrategy underTest = new WaitAllStrategy(WaitAllStrategy.Mode.WITH_INDIVIDUAL_TIMEOUTS_ONLY); assertThat( catchThrowable(() -> { underTest.withStartupTimeout(Duration.ofSeconds(42)); }) ) .as("Cannot change timeout for individual timeouts") .isInstanceOf(IllegalStateException.class); } @Test void shouldNotMessWithIndividualTimeouts() { new WaitAllStrategy(WaitAllStrategy.Mode.WITH_INDIVIDUAL_TIMEOUTS_ONLY) .withStrategy(strategy1) .withStrategy(strategy2); verify(strategy1, never()).withStartupTimeout(any()); verify(strategy1, never()).withStartupTimeout(any()); } @Test void shouldOverwriteIndividualTimeouts() { Duration someSeconds = Duration.ofSeconds(23); new WaitAllStrategy().withStartupTimeout(someSeconds).withStrategy(strategy1).withStrategy(strategy2); verify(strategy1).withStartupTimeout(someSeconds); verify(strategy1).withStartupTimeout(someSeconds); } static class DummyStrategy extends AbstractWaitStrategy { DummyStrategy(Duration defaultInnerWait) { super.startupTimeout = defaultInnerWait; } @Override protected void waitUntilReady() { // no-op } } } ================================================ FILE: core/src/test/java/org/testcontainers/custom/TestCreateContainerCmdModifier.java ================================================ package org.testcontainers.custom; import com.github.dockerjava.api.command.CreateContainerCmd; import org.testcontainers.core.CreateContainerCmdModifier; import java.util.HashMap; import java.util.Map; public class TestCreateContainerCmdModifier implements CreateContainerCmdModifier { @Override public CreateContainerCmd modify(CreateContainerCmd createContainerCmd) { Map labels = new HashMap<>(); labels.put("project", "testcontainers-java"); labels.put("scope", "global"); createContainerCmd.getLabels().putAll(labels); return createContainerCmd; } } ================================================ FILE: core/src/test/java/org/testcontainers/dockerclient/AmbiguousImagePullTest.java ================================================ package org.testcontainers.dockerclient; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.testcontainers.DockerRegistryContainer; import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.DockerImageName; class AmbiguousImagePullTest { @Test @Timeout(30) void testNotUsingParse() { try (DockerRegistryContainer registryContainer = new DockerRegistryContainer()) { registryContainer.start(); DockerImageName imageName = registryContainer.createImage("latest"); String imageNameWithoutTag = imageName.getRegistry() + "/" + imageName.getRepository(); try ( final GenericContainer container = new GenericContainer<>(imageNameWithoutTag).withExposedPorts(8080) ) { container.start(); // do nothing other than start and stop } } } } ================================================ FILE: core/src/test/java/org/testcontainers/dockerclient/DockerClientConfigUtilsTest.java ================================================ package org.testcontainers.dockerclient; import com.github.dockerjava.api.DockerClient; import org.assertj.core.api.Assumptions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.testcontainers.DockerClientFactory; import java.net.URI; import static org.assertj.core.api.Assertions.assertThat; class DockerClientConfigUtilsTest { DockerClient client = DockerClientFactory.lazyClient(); @Test void getDockerHostIpAddressShouldReturnLocalhostWhenUnixSocket() { Assumptions.assumeThat(DockerClientConfigUtils.IN_A_CONTAINER).as("in a container").isFalse(); String actual = DockerClientProviderStrategy.resolveDockerHostIpAddress( client, URI.create("unix:///var/run/docker.sock"), true ); assertThat(actual).isEqualTo("localhost"); } @Test void getDockerHostIpAddressShouldReturnDockerHostIpWhenHttpsUri() { String actual = DockerClientProviderStrategy.resolveDockerHostIpAddress( client, URI.create("http://12.23.34.45"), true ); assertThat(actual).isEqualTo("12.23.34.45"); } @Test void getDockerHostIpAddressShouldReturnDockerHostIpWhenTcpUri() { String actual = DockerClientProviderStrategy.resolveDockerHostIpAddress( client, URI.create("tcp://12.23.34.45"), true ); assertThat(actual).isEqualTo("12.23.34.45"); } @Test void getDockerHostIpAddressShouldReturnNullWhenUnsupportedUriScheme() { String actual = DockerClientProviderStrategy.resolveDockerHostIpAddress( client, URI.create("gopher://12.23.34.45"), true ); assertThat(actual).isNull(); } @Test @Timeout(5) void getDefaultGateway() { assertThat(DockerClientConfigUtils.getDefaultGateway()).isNotNull(); } } ================================================ FILE: core/src/test/java/org/testcontainers/dockerclient/EnvironmentAndSystemPropertyClientProviderStrategyTest.java ================================================ package org.testcontainers.dockerclient; import com.github.dockerjava.core.DefaultDockerClientConfig; import com.github.dockerjava.transport.SSLConfig; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; import org.testcontainers.utility.MockTestcontainersConfigurationExtension; import org.testcontainers.utility.TestcontainersConfiguration; import java.io.IOException; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.util.Properties; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assumptions.assumeThat; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; /** * Test that we can use Testcontainers configuration file to override settings. We assume that docker-java has test * coverage for detection of environment variables (e.g. DOCKER_HOST) and its own properties config file. */ @ExtendWith(MockTestcontainersConfigurationExtension.class) class EnvironmentAndSystemPropertyClientProviderStrategyTest { private URI defaultDockerHost; private com.github.dockerjava.core.SSLConfig defaultSSLConfig; @BeforeEach public void checkEnvironmentClear() { // If docker-java picks up non-default settings from the environment, our test needs to know to expect those DefaultDockerClientConfig defaultConfig = DefaultDockerClientConfig.createDefaultConfigBuilder().build(); defaultDockerHost = defaultConfig.getDockerHost(); defaultSSLConfig = defaultConfig.getSSLConfig(); } @Test void testWhenConfigAbsent() { Mockito .doReturn("auto") .when(TestcontainersConfiguration.getInstance()) .getEnvVarOrProperty(eq("dockerconfig.source"), anyString()); Mockito .doReturn(null) .when(TestcontainersConfiguration.getInstance()) .getEnvVarOrUserProperty(eq("docker.host"), isNull()); Mockito .doReturn(null) .when(TestcontainersConfiguration.getInstance()) .getEnvVarOrUserProperty(eq("docker.tls.verify"), isNull()); Mockito .doReturn(null) .when(TestcontainersConfiguration.getInstance()) .getEnvVarOrUserProperty(eq("docker.cert.path"), isNull()); EnvironmentAndSystemPropertyClientProviderStrategy strategy = new EnvironmentAndSystemPropertyClientProviderStrategy(); TransportConfig transportConfig = strategy.getTransportConfig(); assertThat(transportConfig.getDockerHost()).isEqualTo(defaultDockerHost); assertThat(transportConfig.getSslConfig()).isEqualTo(defaultSSLConfig); } @Test void testWhenDockerHostPresent() { Mockito .doReturn("auto") .when(TestcontainersConfiguration.getInstance()) .getEnvVarOrProperty(eq("dockerconfig.source"), anyString()); Mockito .doReturn("tcp://1.2.3.4:2375") .when(TestcontainersConfiguration.getInstance()) .getEnvVarOrUserProperty(eq("docker.host"), isNull()); Mockito .doReturn(null) .when(TestcontainersConfiguration.getInstance()) .getEnvVarOrUserProperty(eq("docker.tls.verify"), isNull()); Mockito .doReturn(null) .when(TestcontainersConfiguration.getInstance()) .getEnvVarOrUserProperty(eq("docker.cert.path"), isNull()); EnvironmentAndSystemPropertyClientProviderStrategy strategy = new EnvironmentAndSystemPropertyClientProviderStrategy(); TransportConfig transportConfig = strategy.getTransportConfig(); assertThat(transportConfig.getDockerHost().toString()).isEqualTo("tcp://1.2.3.4:2375"); assertThat(transportConfig.getSslConfig()).isEqualTo(defaultSSLConfig); } @Test void testWhenDockerHostAndSSLConfigPresent() throws IOException { Path tempDir = Files.createTempDirectory("testcontainers-test"); String tempDirPath = tempDir.toAbsolutePath().toString(); Mockito .doReturn("auto") .when(TestcontainersConfiguration.getInstance()) .getEnvVarOrProperty(eq("dockerconfig.source"), anyString()); Mockito .doReturn("tcp://1.2.3.4:2375") .when(TestcontainersConfiguration.getInstance()) .getEnvVarOrUserProperty(eq("docker.host"), isNull()); Mockito .doReturn("1") .when(TestcontainersConfiguration.getInstance()) .getEnvVarOrUserProperty(eq("docker.tls.verify"), isNull()); Mockito .doReturn(tempDirPath) .when(TestcontainersConfiguration.getInstance()) .getEnvVarOrUserProperty(eq("docker.cert.path"), isNull()); EnvironmentAndSystemPropertyClientProviderStrategy strategy = new EnvironmentAndSystemPropertyClientProviderStrategy(); TransportConfig transportConfig = strategy.getTransportConfig(); assertThat(transportConfig.getDockerHost().toString()).isEqualTo("tcp://1.2.3.4:2375"); SSLConfig sslConfig = transportConfig.getSslConfig(); assertThat(sslConfig).extracting("dockerCertPath").isEqualTo(tempDirPath); } @Test void applicableWhenIgnoringUserPropertiesAndConfigured() { Mockito .doReturn("autoIgnoringUserProperties") .when(TestcontainersConfiguration.getInstance()) .getEnvVarOrProperty(eq("dockerconfig.source"), anyString()); Properties oldProperties = System.getProperties(); try { System.setProperty("DOCKER_HOST", "tcp://1.2.3.4:2375"); EnvironmentAndSystemPropertyClientProviderStrategy strategy = new EnvironmentAndSystemPropertyClientProviderStrategy(); assertThat(strategy.isApplicable()).isTrue(); } finally { System.setProperties(oldProperties); } } @Test void notApplicableWhenIgnoringUserPropertiesAndNotConfigured() { assumeThat(System.getenv("DOCKER_HOST")).isNull(); Mockito .doReturn("autoIgnoringUserProperties") .when(TestcontainersConfiguration.getInstance()) .getEnvVarOrProperty(eq("dockerconfig.source"), anyString()); EnvironmentAndSystemPropertyClientProviderStrategy strategy = new EnvironmentAndSystemPropertyClientProviderStrategy(); assertThat(strategy.isApplicable()).isFalse(); } } ================================================ FILE: core/src/test/java/org/testcontainers/dockerclient/EventStreamTest.java ================================================ package org.testcontainers.dockerclient; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.model.Event; import com.github.dockerjava.core.command.EventsResultCallback; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.testcontainers.DockerClientFactory; import org.testcontainers.TestImages; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; import java.io.IOException; import java.time.Instant; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** * Test that event streaming from the {@link DockerClient} works correctly */ @Timeout(10) class EventStreamTest { /** * Test that docker events can be streamed from the client. */ @Test void test() throws IOException, InterruptedException { CountDownLatch latch = new CountDownLatch(1); try ( GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) .withCommand("true") .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) ) { container.start(); String createdAt = container.getContainerInfo().getCreated(); // Request all events between startTime and endTime for the container try ( EventsResultCallback response = DockerClientFactory .instance() .client() .eventsCmd() .withContainerFilter(container.getContainerId()) .withEventFilter("create") .withSince(Instant.parse(createdAt).getEpochSecond() + "") .exec( new EventsResultCallback() { @Override public void onNext(@NotNull Event event) { // Check that a create event for the container is received if ( event.getId().equals(container.getContainerId()) && event.getStatus().equals("create") ) { latch.countDown(); } } } ) ) { response.awaitStarted(5, TimeUnit.SECONDS); latch.await(5, TimeUnit.SECONDS); } } } } ================================================ FILE: core/src/test/java/org/testcontainers/dockerclient/ImagePullTest.java ================================================ package org.testcontainers.dockerclient; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; import org.testcontainers.utility.DockerImageName; class ImagePullTest { public static String[] parameters() { return new String[] { "alpine:latest", "alpine:3.17", "alpine", // omitting the tag should work and default to latest "alpine@sha256:1775bebec23e1f3ce486989bfc9ff3c4e951690df84aa9f926497d82f2ffca9d", "docker.io/testcontainers/ryuk:latest", "docker.io/testcontainers/ryuk:0.7.0", "docker.io/testcontainers/ryuk@sha256:bcbee39cd601396958ba1bd06ea14ad64ce0ea709de29a427d741d1f5262080a", // "ibmcom/db2express-c", // Big image for testing with slow networks }; } @ParameterizedTest(name = "{0}") @MethodSource("parameters") void test(String image) { try ( final GenericContainer container = new GenericContainer<>(DockerImageName.parse(image)) .withCommand("/bin/sh", "-c", "sleep 0") .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) ) { container.start(); // do nothing other than start and stop } } } ================================================ FILE: core/src/test/java/org/testcontainers/dockerclient/TestcontainersHostPropertyClientProviderStrategyTest.java ================================================ package org.testcontainers.dockerclient; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; import org.testcontainers.utility.MockTestcontainersConfigurationExtension; import org.testcontainers.utility.TestcontainersConfiguration; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; @ExtendWith(MockTestcontainersConfigurationExtension.class) class TestcontainersHostPropertyClientProviderStrategyTest { @Test void tcHostPropertyIsProvided() { Mockito .doReturn("tcp://127.0.0.1:9000") .when(TestcontainersConfiguration.getInstance()) .getUserProperty(eq("tc.host"), isNull()); TestcontainersHostPropertyClientProviderStrategy strategy = new TestcontainersHostPropertyClientProviderStrategy(); assertThat(strategy.isApplicable()).isTrue(); TransportConfig transportConfig = strategy.getTransportConfig(); assertThat(transportConfig.getDockerHost().toString()).isEqualTo("tcp://127.0.0.1:9000"); } @Test void tcHostPropertyIsNotProvided() { Mockito.doReturn(null).when(TestcontainersConfiguration.getInstance()).getUserProperty(eq("tc.host"), isNull()); TestcontainersHostPropertyClientProviderStrategy strategy = new TestcontainersHostPropertyClientProviderStrategy(); assertThat(strategy.isApplicable()).isFalse(); } } ================================================ FILE: core/src/test/java/org/testcontainers/images/AgeBasedPullPolicyTest.java ================================================ package org.testcontainers.images; import org.junit.jupiter.api.Test; import org.testcontainers.utility.DockerImageName; import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; class AgeBasedPullPolicyTest { final DockerImageName imageName = DockerImageName.parse(UUID.randomUUID().toString()); @Test void shouldPull() { ImageData imageData = ImageData.builder().createdAt(Instant.now().minus(2, ChronoUnit.HOURS)).build(); AgeBasedPullPolicy oneHour = new AgeBasedPullPolicy(Duration.of(1L, ChronoUnit.HOURS)); assertThat(oneHour.shouldPullCached(imageName, imageData)).isTrue(); AgeBasedPullPolicy fiveHours = new AgeBasedPullPolicy(Duration.of(5L, ChronoUnit.HOURS)); assertThat(fiveHours.shouldPullCached(imageName, imageData)).isFalse(); } } ================================================ FILE: core/src/test/java/org/testcontainers/images/ImageDataTest.java ================================================ package org.testcontainers.images; import com.github.dockerjava.api.command.InspectImageResponse; import org.junit.jupiter.api.Test; import java.time.Instant; import static org.assertj.core.api.Assertions.assertThat; class ImageDataTest { @Test void shouldReadTimestampWithoutOffsetFromInspectImageResponse() { final String timestamp = "2020-07-27T18:23:31.365190246Z"; final ImageData imageData = ImageData.from(new InspectImageResponse().withCreated(timestamp)); assertThat(imageData.getCreatedAt()).isEqualTo(Instant.parse(timestamp)); } @Test void shouldReadTimestampWithOffsetFromInspectImageResponse() { final String timestamp = "2020-07-27T18:23:31.365190246+02:00"; final ImageData imageData = ImageData.from(new InspectImageResponse().withCreated(timestamp)); assertThat(imageData.getCreatedAt()).isEqualTo(Instant.parse("2020-07-27T16:23:31.365190246Z")); } } ================================================ FILE: core/src/test/java/org/testcontainers/images/ImagePullPolicyTest.java ================================================ package org.testcontainers.images; import com.github.dockerjava.api.exception.NotFoundException; import org.junit.jupiter.api.AutoClose; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.testcontainers.DockerClientFactory; import org.testcontainers.DockerRegistryContainer; import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; import org.testcontainers.utility.DockerImageName; import static org.assertj.core.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; class ImagePullPolicyTest { @AutoClose public static DockerRegistryContainer registry = new DockerRegistryContainer(); private final DockerImageName imageName = registry.createImage(); static { registry.start(); } @Test void pullsByDefault() { try (GenericContainer container = new GenericContainer<>(imageName).withExposedPorts(8080)) { container.start(); } } @Test void shouldAlwaysPull() { try (GenericContainer container = new GenericContainer<>(imageName).withExposedPorts(8080)) { container.start(); } removeImage(); try (GenericContainer container = new GenericContainer<>(imageName).withExposedPorts(8080)) { expectToFailWithNotFoundException(container); } try ( // built_in_image_pull_policy { GenericContainer container = new GenericContainer<>(imageName) .withImagePullPolicy(PullPolicy.alwaysPull()) // } ) { container.withExposedPorts(8080); container.start(); } } @Test void shouldSupportCustomPolicies() { try ( // custom_image_pull_policy { GenericContainer container = new GenericContainer<>(imageName) .withImagePullPolicy( new AbstractImagePullPolicy() { @Override protected boolean shouldPullCached(DockerImageName imageName, ImageData localImageData) { return System.getenv("ALWAYS_PULL_IMAGE") != null; } } ) // } ) { container.withExposedPorts(8080); container.start(); } } @Test void shouldCheckPolicy() { ImagePullPolicy policy = Mockito.spy( new AbstractImagePullPolicy() { @Override protected boolean shouldPullCached(DockerImageName imageName, ImageData localImageData) { return false; } } ); try ( GenericContainer container = new GenericContainer<>(imageName) .withImagePullPolicy(policy) .withExposedPorts(8080) ) { container.start(); Mockito.verify(policy).shouldPull(any()); } } @Test void shouldNotForcePulling() { try ( GenericContainer container = new GenericContainer<>(imageName) .withImagePullPolicy(__ -> false) .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) ) { expectToFailWithNotFoundException(container); } } private void expectToFailWithNotFoundException(GenericContainer container) { try { container.start(); fail("Should fail"); } catch (ContainerLaunchException e) { Throwable throwable = e; while (throwable.getCause() != null) { throwable = throwable.getCause(); if (throwable.getCause() instanceof NotFoundException) { return; } } fail("Caused by NotFoundException"); } } private void removeImage() { try { DockerClientFactory .instance() .client() .removeImageCmd(imageName.asCanonicalNameString()) .withForce(true) .exec(); } catch (NotFoundException ignored) {} } } ================================================ FILE: core/src/test/java/org/testcontainers/images/LocalImagesCacheAccessor.java ================================================ package org.testcontainers.images; public final class LocalImagesCacheAccessor { public static synchronized void clearCache() { LocalImagesCache.INSTANCE.cache.clear(); LocalImagesCache.INSTANCE.initialized.set(false); } } ================================================ FILE: core/src/test/java/org/testcontainers/images/OverrideImagePullPolicyTest.java ================================================ package org.testcontainers.images; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; import org.testcontainers.DockerRegistryContainer; import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.FakeImagePullPolicy; import org.testcontainers.utility.MockTestcontainersConfigurationExtension; import org.testcontainers.utility.TestcontainersConfiguration; import static org.assertj.core.api.Assertions.assertThat; @ExtendWith(MockTestcontainersConfigurationExtension.class) class OverrideImagePullPolicyTest { private ImagePullPolicy originalInstance; private ImagePullPolicy originalDefaultImplementation; @BeforeEach public void setUp() { this.originalInstance = PullPolicy.instance; this.originalDefaultImplementation = PullPolicy.defaultImplementation; PullPolicy.instance = null; PullPolicy.defaultImplementation = Mockito.mock(ImagePullPolicy.class); } @AfterEach public void tearDown() { PullPolicy.instance = originalInstance; PullPolicy.defaultImplementation = originalDefaultImplementation; } @Test void simpleConfigurationTest() { Mockito .doReturn(FakeImagePullPolicy.class.getCanonicalName()) .when(TestcontainersConfiguration.getInstance()) .getImagePullPolicy(); try (DockerRegistryContainer registry = new DockerRegistryContainer()) { registry.start(); GenericContainer container = new GenericContainer<>(registry.createImage()).withExposedPorts(8080); container.start(); assertThat(container.getImage().imagePullPolicy).isInstanceOf(FakeImagePullPolicy.class); container.stop(); } } @Test void alwaysPullConfigurationTest() { Mockito .doReturn(AlwaysPullPolicy.class.getCanonicalName()) .when(TestcontainersConfiguration.getInstance()) .getImagePullPolicy(); try (DockerRegistryContainer registry = new DockerRegistryContainer()) { registry.start(); GenericContainer container = new GenericContainer<>(registry.createImage()).withExposedPorts(8080); container.start(); assertThat(container.getImage().imagePullPolicy).isInstanceOf(AlwaysPullPolicy.class); container.stop(); } } } ================================================ FILE: core/src/test/java/org/testcontainers/images/ParsedDockerfileTest.java ================================================ package org.testcontainers.images; import com.google.common.collect.Sets; import org.junit.jupiter.api.Test; import java.nio.file.Paths; import java.util.Arrays; import static org.assertj.core.api.Assertions.assertThat; class ParsedDockerfileTest { @Test void doesSimpleParsing() { final ParsedDockerfile parsedDockerfile = new ParsedDockerfile( Arrays.asList("FROM someimage", "RUN something") ); assertThat(parsedDockerfile.getDependencyImageNames()) .as("extracts a single image name") .isEqualTo(Sets.newHashSet("someimage")); } @Test void isCaseInsensitive() { final ParsedDockerfile parsedDockerfile = new ParsedDockerfile( Arrays.asList("from someimage", "RUN something") ); assertThat(parsedDockerfile.getDependencyImageNames()) .as("extracts a single image name") .isEqualTo(Sets.newHashSet("someimage")); } @Test void handlesTags() { final ParsedDockerfile parsedDockerfile = new ParsedDockerfile( Arrays.asList("FROM someimage:tag", "RUN something") ); assertThat(parsedDockerfile.getDependencyImageNames()) .as("retains tags in image names") .isEqualTo(Sets.newHashSet("someimage:tag")); } @Test void handlesDigests() { final ParsedDockerfile parsedDockerfile = new ParsedDockerfile( Arrays.asList("FROM someimage@sha256:abc123", "RUN something") ); assertThat(parsedDockerfile.getDependencyImageNames()) .as("retains digests in image names") .isEqualTo(Sets.newHashSet("someimage@sha256:abc123")); } @Test void ignoringCommentedFromLines() { final ParsedDockerfile parsedDockerfile = new ParsedDockerfile( Arrays.asList("FROM someimage", "#FROM somethingelse") ); assertThat(parsedDockerfile.getDependencyImageNames()) .as("ignores commented from lines") .isEqualTo(Sets.newHashSet("someimage")); } @Test void ignoringBuildStageNames() { final ParsedDockerfile parsedDockerfile = new ParsedDockerfile( Arrays.asList("FROM someimage --as=base", "RUN something", "FROM nextimage", "RUN something") ); assertThat(parsedDockerfile.getDependencyImageNames()) .as("ignores build stage names and allows multiple images to be extracted") .isEqualTo(Sets.newHashSet("someimage", "nextimage")); } @Test void ignoringPlatformArgs() { final ParsedDockerfile parsedDockerfile = new ParsedDockerfile( Arrays.asList("FROM --platform=linux/amd64 someimage", "RUN something") ); assertThat(parsedDockerfile.getDependencyImageNames()) .as("ignores platform args") .isEqualTo(Sets.newHashSet("someimage")); } @Test void ignoringExtraPlatformArgs() { final ParsedDockerfile parsedDockerfile = new ParsedDockerfile( Arrays.asList("FROM --platform=linux/amd64 --somethingelse=value someimage", "RUN something") ); assertThat(parsedDockerfile.getDependencyImageNames()) .as("ignores platform args") .isEqualTo(Sets.newHashSet("someimage")); } @Test void handlesGracefullyIfNoFromLine() { final ParsedDockerfile parsedDockerfile = new ParsedDockerfile( Arrays.asList("RUN something", "# is this even a valid Dockerfile?") ); assertThat(parsedDockerfile.getDependencyImageNames()) .as("handles invalid Dockerfiles gracefully") .isEqualTo(Sets.newHashSet()); } @Test void handlesGracefullyIfDockerfileNotFound() { final ParsedDockerfile parsedDockerfile = new ParsedDockerfile(Paths.get("nonexistent.Dockerfile")); assertThat(parsedDockerfile.getDependencyImageNames()) .as("handles missing Dockerfiles gracefully") .isEqualTo(Sets.newHashSet()); } } ================================================ FILE: core/src/test/java/org/testcontainers/images/RemoteDockerImageTest.java ================================================ package org.testcontainers.images; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.testcontainers.utility.Base58; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.LazyFuture; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; import static org.assertj.core.api.Assertions.assertThat; class RemoteDockerImageTest { @Test void toStringContainsOnlyImageName() { String imageName = Base58.randomString(8).toLowerCase(); RemoteDockerImage remoteDockerImage = new RemoteDockerImage(DockerImageName.parse(imageName)); assertThat(remoteDockerImage.toString()).contains("imageName=" + imageName); } @Test void toStringWithExceptionContainsOnlyImageNameFuture() { CompletableFuture imageNameFuture = new CompletableFuture<>(); imageNameFuture.completeExceptionally(new RuntimeException("arbitrary")); RemoteDockerImage remoteDockerImage = new RemoteDockerImage(imageNameFuture); assertThat(remoteDockerImage.toString()).contains("imageName=java.lang.RuntimeException: arbitrary"); } @Test @Timeout(5) void toStringDoesntResolveImageNameFuture() { CompletableFuture imageNameFuture = new CompletableFuture<>(); // verify that we've set up the test properly assertThat(imageNameFuture).isNotDone(); RemoteDockerImage remoteDockerImage = new RemoteDockerImage(imageNameFuture); assertThat(remoteDockerImage.toString()).contains("imageName="); // Make sure the act of calling toString doesn't resolve the imageNameFuture assertThat(imageNameFuture).isNotDone(); String imageName = Base58.randomString(8).toLowerCase(); imageNameFuture.complete(imageName); assertThat(remoteDockerImage.toString()).contains("imageName=" + imageName); } @Test @Timeout(5) void toStringDoesntResolveLazyFuture() throws Exception { String imageName = Base58.randomString(8).toLowerCase(); AtomicBoolean resolved = new AtomicBoolean(false); Future imageNameFuture = new LazyFuture() { @Override protected String resolve() { resolved.set(true); return imageName; } }; // verify that we've set up the test properly assertThat(imageNameFuture).isNotDone(); RemoteDockerImage remoteDockerImage = new RemoteDockerImage(imageNameFuture); assertThat(remoteDockerImage.toString()).contains("imageName="); // Make sure the act of calling toString doesn't resolve the imageNameFuture assertThat(imageNameFuture).isNotDone(); assertThat(resolved).isFalse(); // Trigger resolve imageNameFuture.get(); assertThat(remoteDockerImage.toString()).contains("imageName=" + imageName); } } ================================================ FILE: core/src/test/java/org/testcontainers/images/builder/DockerfileBuildTest.java ================================================ package org.testcontainers.images.builder; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; import java.nio.file.Path; import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; class DockerfileBuildTest { static final Path RESOURCE_PATH = Paths.get("src/test/resources/dockerfile-build-test"); public static Stream parameters() { Map buildArgs = new HashMap<>(4); buildArgs.put("BUILD_IMAGE", "alpine:3.16"); buildArgs.put("BASE_IMAGE", "alpine"); buildArgs.put("BASE_IMAGE_TAG", "3.12"); buildArgs.put("UNUSED", "ignored"); //noinspection deprecation return Stream.of( // Dockerfile build without explicit per-file inclusion Arguments.of( "test1234", // spotless:off // docsShowRecursiveFileInclusion { new ImageFromDockerfile() .withFileFromPath(".", RESOURCE_PATH)), // } // spotless:on // Dockerfile build using a non-standard Dockerfile Arguments.of( "test4567", new ImageFromDockerfile().withFileFromPath(".", RESOURCE_PATH).withDockerfilePath("./Dockerfile-alt") ), // Dockerfile build using withBuildArg() Arguments.of( "test7890", new ImageFromDockerfile() .withFileFromPath(".", RESOURCE_PATH) .withDockerfilePath("./Dockerfile-buildarg") .withBuildArg("CUSTOM_ARG", "test7890") ), // Dockerfile build using withBuildArgs() with build args in FROM statement Arguments.of( "test1234", new ImageFromDockerfile() .withFileFromPath(".", RESOURCE_PATH) .withDockerfile(RESOURCE_PATH.resolve("Dockerfile-from-buildarg")) .withBuildArgs(buildArgs) ), // Dockerfile build using withDockerfile(File) Arguments.of( "test4567", new ImageFromDockerfile() .withFileFromPath(".", RESOURCE_PATH) .withDockerfile(RESOURCE_PATH.resolve("Dockerfile-alt")) ) ); } @ParameterizedTest @MethodSource("parameters") void performTest(String expectedFileContent, ImageFromDockerfile image) { try ( final GenericContainer container = new GenericContainer<>(image) .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) .withCommand("cat", "/test.txt") ) { container.start(); final String logs = container.getLogs(); assertThat(logs) .as("expected file content indicates that dockerfile build steps have been run") .contains(expectedFileContent); } } } ================================================ FILE: core/src/test/java/org/testcontainers/images/builder/DockerignoreTest.java ================================================ package org.testcontainers.images.builder; import com.github.dockerjava.api.exception.DockerClientException; import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; import org.testcontainers.utility.DockerImageName; import java.nio.file.Path; import java.nio.file.Paths; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; class DockerignoreTest { private static final Path INVALID_DOCKERIGNORE_PATH = Paths.get("src/test/resources/dockerfile-build-invalid"); @Test void testInvalidDockerignore() throws Exception { try { new ImageFromDockerfile() .withFileFromPath(".", INVALID_DOCKERIGNORE_PATH) .withDockerfile(INVALID_DOCKERIGNORE_PATH.resolve("Dockerfile")) .get(); fail("Should not be able to build an image with an invalid .dockerignore file"); } catch (DockerClientException e) { if (!e.getMessage().contains("Invalid pattern")) { throw e; } } } @SuppressWarnings("resource") @Test void testValidDockerignore() throws Exception { ImageFromDockerfile img = new ImageFromDockerfile() .withFileFromPath(".", DockerfileBuildTest.RESOURCE_PATH) .withDockerfile(DockerfileBuildTest.RESOURCE_PATH.resolve("Dockerfile-currentdir")); try ( final GenericContainer container = new GenericContainer(DockerImageName.parse(img.get())) .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) .withCommand("ls", "/") ) { container.start(); final String logs = container.getLogs(); assertThat(logs) .as("Files in the container indicated the .dockerignore was not applied. Output was: " + logs) .contains("should_not_be_ignored.txt"); assertThat(logs) .as("Files in the container indicated the .dockerignore was not applied. Output was: " + logs) .doesNotContain("should_be_ignored.txt"); } } } ================================================ FILE: core/src/test/java/org/testcontainers/images/builder/ImageFromDockerfileTest.java ================================================ package org.testcontainers.images.builder; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.InspectImageResponse; import org.junit.jupiter.api.Test; import org.testcontainers.DockerClientFactory; import org.testcontainers.utility.Base58; import static org.assertj.core.api.Assertions.assertThat; class ImageFromDockerfileTest { @Test void shouldAddDefaultLabels() { ImageFromDockerfile image = new ImageFromDockerfile().withDockerfileFromBuilder(it -> it.from("scratch")); String imageId = image.resolve(); DockerClient dockerClient = DockerClientFactory.instance().client(); InspectImageResponse inspectImageResponse = dockerClient.inspectImageCmd(imageId).exec(); assertThat(inspectImageResponse.getConfig().getLabels()) .containsAllEntriesOf(DockerClientFactory.DEFAULT_LABELS); } @Test void shouldNotAddSessionLabelIfDeleteOnExitIsFalse() { ImageFromDockerfile image = new ImageFromDockerfile( "localhost/testcontainers/" + Base58.randomString(16).toLowerCase(), false ) .withDockerfileFromBuilder(it -> it.from("scratch")); String imageId = image.resolve(); DockerClient dockerClient = DockerClientFactory.instance().client(); try { InspectImageResponse inspectImageResponse = dockerClient.inspectImageCmd(imageId).exec(); assertThat(inspectImageResponse.getConfig().getLabels()) .doesNotContainKey(DockerClientFactory.TESTCONTAINERS_SESSION_ID_LABEL); } finally { // ensure the image is deleted, even if the test fails dockerClient.removeImageCmd(imageId).exec(); } } } ================================================ FILE: core/src/test/java/org/testcontainers/images/builder/dockerfile/statement/AbstractStatementTest.java ================================================ package org.testcontainers.images.builder.dockerfile.statement; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.junit.jupiter.api.TestInfo; import org.rnorth.ducttape.Preconditions; import java.io.InputStream; import java.util.Arrays; import static org.assertj.core.api.Assertions.fail; public abstract class AbstractStatementTest { private final TestInfo testInfo; AbstractStatementTest(TestInfo testInfo) { this.testInfo = testInfo; } protected void assertStatement(Statement statement) { String testName = testInfo.getTestMethod().get().getName(); String[] expectedLines = new String[0]; try { String path = "fixtures/statements/" + getClass().getSimpleName() + "/" + testName; InputStream inputStream = getClass().getClassLoader().getResourceAsStream(path); Preconditions.check("inputStream is null for path " + path, inputStream != null); String content = IOUtils.toString(inputStream); IOUtils.closeQuietly(inputStream); expectedLines = StringUtils.chomp(content.replaceAll("\r\n", "\n").trim()).split("\n"); } catch (Exception e) { fail("can't load fixture '" + testName + "'\n" + ExceptionUtils.getStackTrace(e)); } StringBuilder builder = new StringBuilder(); statement.appendArguments(builder); String[] resultLines = StringUtils.chomp(builder.toString().trim()).split("\n"); if (expectedLines.length != resultLines.length) { fail( "number of lines is not the same. Expected " + expectedLines.length + " but got " + resultLines.length ); } if (!Arrays.equals(expectedLines, resultLines)) { StringBuilder failureBuilder = new StringBuilder(); failureBuilder.append("Invalid statement!\n"); for (int i = 0; i < expectedLines.length; i++) { String expectedLine = expectedLines[i]; String actualLine = resultLines[i]; if (!expectedLine.equals(actualLine)) { failureBuilder.append("Invalid line #"); failureBuilder.append(i); failureBuilder.append(":\n\tActual: <"); failureBuilder.append(actualLine); failureBuilder.append(">\n\tExpected: <"); failureBuilder.append(expectedLine); failureBuilder.append(">\n"); } } fail(failureBuilder.toString()); } } } ================================================ FILE: core/src/test/java/org/testcontainers/images/builder/dockerfile/statement/KeyValuesStatementTest.java ================================================ package org.testcontainers.images.builder.dockerfile.statement; import com.google.common.collect.ImmutableMap; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import java.util.Collections; class KeyValuesStatementTest extends AbstractStatementTest { KeyValuesStatementTest(TestInfo testInfo) { super(testInfo); } @Test void multilineTest() throws Exception { ImmutableMap pairs = ImmutableMap .builder() .put("line1", "1") .put("line2", "2") .put("line3", "3") .build(); assertStatement(new KeyValuesStatement("TEST", pairs)); } @Test void keyWithSpacesTest() throws Exception { assertStatement(new KeyValuesStatement("TEST", Collections.singletonMap("key with spaces", "1"))); } @Test void keyWithNewLinesTest() throws Exception { assertStatement(new KeyValuesStatement("TEST", Collections.singletonMap("key\nwith\nnewlines", "1"))); } @Test void keyWithTabsTest() throws Exception { assertStatement(new KeyValuesStatement("TEST", Collections.singletonMap("key\twith\ttab", "1"))); } @Test void valueIsEscapedTest() throws Exception { ImmutableMap pairs = ImmutableMap .builder() .put("1", "value with spaces") .put("2", "value\nwith\nnewlines") .put("3", "value\twith\ttab") .build(); assertStatement(new KeyValuesStatement("TEST", pairs)); } } ================================================ FILE: core/src/test/java/org/testcontainers/images/builder/dockerfile/statement/MultiArgsStatementTest.java ================================================ package org.testcontainers.images.builder.dockerfile.statement; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; class MultiArgsStatementTest extends AbstractStatementTest { MultiArgsStatementTest(TestInfo testInfo) { super(testInfo); } @Test void simpleTest() { assertStatement(new MultiArgsStatement("TEST", "a", "b", "c")); } @Test void multilineTest() { assertStatement(new MultiArgsStatement("TEST", "some\nmultiline\nargument")); } } ================================================ FILE: core/src/test/java/org/testcontainers/images/builder/dockerfile/statement/RawStatementTest.java ================================================ package org.testcontainers.images.builder.dockerfile.statement; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; class RawStatementTest extends AbstractStatementTest { RawStatementTest(TestInfo testInfo) { super(testInfo); } @Test void simpleTest() throws Exception { assertStatement(new RawStatement("TEST", "value\nas\t\\\nis")); } } ================================================ FILE: core/src/test/java/org/testcontainers/images/builder/dockerfile/statement/SingleArgumentStatementTest.java ================================================ package org.testcontainers.images.builder.dockerfile.statement; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; class SingleArgumentStatementTest extends AbstractStatementTest { SingleArgumentStatementTest(TestInfo testInfo) { super(testInfo); } @Test void simpleTest() throws Exception { assertStatement(new SingleArgumentStatement("TEST", "hello")); } @Test void multilineTest() throws Exception { assertStatement(new SingleArgumentStatement("TEST", "hello\nworld")); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/BaseComposeTest.java ================================================ package org.testcontainers.junit; import com.github.dockerjava.api.model.Network; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.ComposeContainer; import org.testcontainers.utility.TestEnvironment; import redis.clients.jedis.Jedis; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; public abstract class BaseComposeTest { protected static final int REDIS_PORT = 6379; protected abstract ComposeContainer getEnvironment(); private List existingNetworks = new ArrayList<>(); @BeforeAll public static void checkVersion() { Assumptions.assumeTrue(TestEnvironment.dockerApiAtLeast("1.22")); } @Test void simpleTest() { Jedis jedis = new Jedis( getEnvironment().getServiceHost("redis-1", REDIS_PORT), getEnvironment().getServicePort("redis-1", REDIS_PORT) ); jedis.incr("test"); jedis.incr("test"); jedis.incr("test"); assertThat(jedis.get("test")).as("A redis instance defined in compose can be used in isolation").isEqualTo("3"); } @Test void secondTest() { // used in manual checking for cleanup in between tests Jedis jedis = new Jedis( getEnvironment().getServiceHost("redis-1", REDIS_PORT), getEnvironment().getServicePort("redis-1", REDIS_PORT) ); jedis.incr("test"); jedis.incr("test"); jedis.incr("test"); assertThat(jedis.get("test")).as("Tests use fresh container instances").isEqualTo("3"); // if these end up using the same container one of the test methods will fail. // However, @Rule creates a separate ComposeContainer instance per test, so this just shouldn't happen } @BeforeEach public void captureNetworks() { existingNetworks.addAll(findAllNetworks()); } @AfterEach public void verifyNoNetworks() { assertThat(findAllNetworks()).as("The networks").isEqualTo(existingNetworks); } private List findAllNetworks() { return DockerClientFactory .instance() .client() .listNetworksCmd() .exec() .stream() .map(Network::getName) .sorted() .collect(Collectors.toList()); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/BaseDockerComposeTest.java ================================================ package org.testcontainers.junit; import com.github.dockerjava.api.model.Network; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.utility.TestEnvironment; import redis.clients.jedis.Jedis; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assumptions.assumeThat; /** * Created by rnorth on 21/05/2016. */ public abstract class BaseDockerComposeTest { protected static final int REDIS_PORT = 6379; protected abstract DockerComposeContainer getEnvironment(); private List existingNetworks = new ArrayList<>(); @BeforeAll public static void checkVersion() { assumeThat(TestEnvironment.dockerApiAtLeast("1.22")).isTrue(); } @Test void simpleTest() { Jedis jedis = new Jedis( getEnvironment().getServiceHost("redis_1", REDIS_PORT), getEnvironment().getServicePort("redis_1", REDIS_PORT) ); jedis.incr("test"); jedis.incr("test"); jedis.incr("test"); assertThat(jedis.get("test")).as("A redis instance defined in compose can be used in isolation").isEqualTo("3"); } @Test void secondTest() { // used in manual checking for cleanup in between tests Jedis jedis = new Jedis( getEnvironment().getServiceHost("redis_1", REDIS_PORT), getEnvironment().getServicePort("redis_1", REDIS_PORT) ); jedis.incr("test"); jedis.incr("test"); jedis.incr("test"); assertThat(jedis.get("test")).as("Tests use fresh container instances").isEqualTo("3"); // if these end up using the same container one of the test methods will fail. // However, @Rule creates a separate DockerComposeContainer instance per test, so this just shouldn't happen } @BeforeEach public void captureNetworks() { existingNetworks.addAll(findAllNetworks()); } @AfterEach public void verifyNoNetworks() { assertThat(findAllNetworks()).as("The networks").isEqualTo(existingNetworks); } private List findAllNetworks() { return DockerClientFactory .instance() .client() .listNetworksCmd() .exec() .stream() .map(Network::getName) .sorted() .collect(Collectors.toList()); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/ComposeContainerOverrideTest.java ================================================ package org.testcontainers.junit; import com.github.dockerjava.api.command.InspectContainerResponse; import org.junit.jupiter.api.Test; import org.testcontainers.containers.ComposeContainer; import org.testcontainers.containers.ContainerState; import org.testcontainers.utility.DockerImageName; import java.io.File; import static org.assertj.core.api.Assertions.assertThat; class ComposeContainerOverrideTest { private static final File BASE = new File("src/test/resources/compose-override/compose.yml"); private static final File OVERRIDE = new File("src/test/resources/compose-override/compose-override.yml"); @Test void readEnvironment() { try ( ComposeContainer compose = new ComposeContainer(DockerImageName.parse("docker:25.0.5"), BASE) .withExposedService("redis", 6379) ) { compose.start(); InspectContainerResponse container = compose .getContainerByServiceName("redis-1") .map(ContainerState::getContainerInfo) .get(); assertThat(container.getConfig().getEnv()).contains("foo=bar"); } } @Test void resetEnvironment() { try ( ComposeContainer compose = new ComposeContainer(DockerImageName.parse("docker:25.0.5"), BASE, OVERRIDE) .withExposedService("redis", 6379) ) { compose.start(); InspectContainerResponse container = compose .getContainerByServiceName("redis-1") .map(ContainerState::getContainerInfo) .get(); assertThat(container.getConfig().getEnv()).doesNotContain("foo=bar"); } } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/ComposeContainerPortViaEnvTest.java ================================================ package org.testcontainers.junit; import org.junit.jupiter.api.AutoClose; import org.testcontainers.containers.ComposeContainer; import org.testcontainers.utility.DockerImageName; import java.io.File; class ComposeContainerPortViaEnvTest extends BaseComposeTest { @AutoClose public ComposeContainer environment = new ComposeContainer( DockerImageName.parse("docker:25.0.5"), new File("src/test/resources/v2-compose-test-port-via-env.yml") ) .withExposedService("redis-1", REDIS_PORT) .withEnv("REDIS_PORT", String.valueOf(REDIS_PORT)); ComposeContainerPortViaEnvTest() { this.environment.start(); } @Override protected ComposeContainer getEnvironment() { return environment; } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/ComposeContainerScalingTest.java ================================================ package org.testcontainers.junit; import org.assertj.core.api.Assumptions; import org.junit.jupiter.api.AutoClose; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.testcontainers.containers.ComposeContainer; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.TestEnvironment; import redis.clients.jedis.Jedis; import java.io.File; import static org.assertj.core.api.Assertions.assertThat; class ComposeContainerScalingTest { private static final int REDIS_PORT = 6379; private Jedis[] clients = new Jedis[3]; @BeforeAll public static void checkVersion() { Assumptions.assumeThat(TestEnvironment.dockerApiAtLeast("1.22")).isTrue(); } @AutoClose public ComposeContainer environment = new ComposeContainer( DockerImageName.parse("docker:25.0.5"), new File("src/test/resources/composev2/scaled-compose-test.yml") ) .withScaledService("redis", 3) .withExposedService("redis", REDIS_PORT) // implicit '-1' .withExposedService("redis-2", REDIS_PORT) // explicit service index .withExposedService("redis", 3, REDIS_PORT); // explicit service index via parameter ComposeContainerScalingTest() { environment.start(); } @BeforeEach public void setupClients() { for (int i = 0; i < 3; i++) { String name = String.format("redis-%d", i + 1); clients[i] = new Jedis(environment.getServiceHost(name, REDIS_PORT), environment.getServicePort(name, REDIS_PORT)); } } @Test void simpleTest() { for (int i = 0; i < 3; i++) { clients[i].incr("somekey"); assertThat(clients[i].get("somekey")).as("Each redis instance is separate").isEqualTo("1"); } } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/ComposeContainerTest.java ================================================ package org.testcontainers.junit; import org.junit.jupiter.api.Test; import org.testcontainers.containers.ComposeContainer; import org.testcontainers.containers.ContainerState; import org.testcontainers.utility.DockerImageName; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.Collections; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; class ComposeContainerTest extends BaseComposeTest { // composeContainerConstructor { public ComposeContainer environment = new ComposeContainer( DockerImageName.parse("docker:25.0.5"), new File("src/test/resources/composev2/compose-test.yml") ) .withExposedService("redis-1", REDIS_PORT) .withExposedService("db-1", 3306); // } ComposeContainerTest() { environment.start(); } @Override protected ComposeContainer getEnvironment() { return environment; } @Test 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); } @Test void shouldRetrieveContainerByServiceName() { String existingServiceName = "db-1"; Optional result = environment.getContainerByServiceName(existingServiceName); assertThat(result) .as(String.format("Container should be found by service name %s", existingServiceName)) .isPresent(); assertThat(Collections.singletonList(3306)) .as("Mapped port for result container was wrong, probably wrong container found") .isEqualTo(result.get().getExposedPorts()); } @Test void shouldReturnEmptyResultOnNoneExistingService() { String notExistingServiceName = "db-256"; Optional result = environment.getContainerByServiceName(notExistingServiceName); assertThat(result) .as(String.format("No container should be found under service name %s", notExistingServiceName)) .isNotPresent(); } @Test void shouldCreateContainerWhenFileNotPrefixedWithPath() throws IOException { String validYaml = "version: '2.2'\n" + "services:\n" + " http:\n" + " build: .\n" + " image: python:latest\n" + " ports:\n" + " - 8080:8080"; File filePathNotStartWithDotSlash = new File("docker-compose-test.yml"); filePathNotStartWithDotSlash.createNewFile(); filePathNotStartWithDotSlash.deleteOnExit(); Files.write(filePathNotStartWithDotSlash.toPath(), validYaml.getBytes(StandardCharsets.UTF_8)); final ComposeContainer dockerComposeContainer = new ComposeContainer( DockerImageName.parse("docker:25.0.5"), filePathNotStartWithDotSlash ); assertThat(dockerComposeContainer).as("Container created using docker compose file").isNotNull(); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/ComposeContainerVolumeRemovalTest.java ================================================ package org.testcontainers.junit; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.ComposeContainer; import org.testcontainers.utility.DockerImageName; import java.io.File; import java.util.LinkedHashSet; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @Disabled class ComposeContainerVolumeRemovalTest { public static Stream params() { return Stream.of(Arguments.of(true, false), Arguments.of(false, true)); } @ParameterizedTest @MethodSource("params") void performTest(boolean removeVolumes, boolean shouldVolumesBePresentAfterRunning) { final File composeFile = new File("src/test/resources/v2-compose-test.yml"); final AtomicReference volumeName = new AtomicReference<>(""); try ( ComposeContainer environment = new ComposeContainer(DockerImageName.parse("docker:25.0.5"), composeFile) .withExposedService("redis", 6379) .withRemoveVolumes(removeVolumes) .withRemoveImages(ComposeContainer.RemoveImages.ALL) ) { environment.start(); volumeName.set(volumeNameForRunningContainer("-redis-1")); final boolean isVolumePresentWhileRunning = isVolumePresent(volumeName.get()); assertThat(isVolumePresentWhileRunning).as("the container volume is present while running").isEqualTo(true); } await() .untilAsserted(() -> { final boolean isVolumePresentAfterRunning = isVolumePresent(volumeName.get()); assertThat(isVolumePresentAfterRunning) .as("the container volume is present after running") .isEqualTo(shouldVolumesBePresentAfterRunning); }); } private String volumeNameForRunningContainer(final String containerNameSuffix) { return DockerClientFactory .instance() .client() .listContainersCmd() .exec() .stream() .filter(it -> Stream.of(it.getNames()).anyMatch(name -> name.endsWith(containerNameSuffix))) .findFirst() .map(container -> container.getMounts().get(0).getName()) .orElseThrow(IllegalStateException::new); } private boolean isVolumePresent(final String volumeName) { Set nameFilter = new LinkedHashSet<>(1); nameFilter.add(volumeName); return DockerClientFactory .instance() .client() .listVolumesCmd() .withFilter("name", nameFilter) .exec() .getVolumes() .stream() .findFirst() .isPresent(); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/ComposeContainerWithBuildTest.java ================================================ package org.testcontainers.junit; import com.github.dockerjava.api.model.Container; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.rnorth.ducttape.unreliables.Unreliables; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.ComposeContainer; import org.testcontainers.utility.DockerImageName; import java.io.File; import java.util.Collections; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; class ComposeContainerWithBuildTest { public static Stream params() { return Stream.of( Arguments.of(null, true, true), Arguments.of(ComposeContainer.RemoveImages.LOCAL, false, true), Arguments.of(ComposeContainer.RemoveImages.ALL, false, false) ); } @ParameterizedTest @MethodSource("params") void performTest( ComposeContainer.RemoveImages removeMode, boolean shouldBuiltImageBePresentAfterRunning, boolean shouldPulledImageBePresentAfterRunning ) { final File composeFile = new File("src/test/resources/compose-v2-build-test/docker-compose.yml"); final AtomicReference builtImageName = new AtomicReference<>(""); final AtomicReference pulledImageName = new AtomicReference<>(""); try ( ComposeContainer environment = new ComposeContainer(DockerImageName.parse("docker:25.0.5"), composeFile) .withExposedService("customredis", 6379) .withBuild(true) .withRemoveImages(removeMode) ) { environment.start(); builtImageName.set(imageNameForRunningContainer("-customredis-1")); final boolean isBuiltImagePresentWhileRunning = isImagePresent(builtImageName.get()); assertThat(isBuiltImagePresentWhileRunning).as("the built image is present while running").isTrue(); pulledImageName.set(imageNameForRunningContainer("-normalredis-1")); final boolean isPulledImagePresentWhileRunning = isImagePresent(pulledImageName.get()); assertThat(isPulledImagePresentWhileRunning).as("the pulled image is present while running").isTrue(); } Unreliables.retryUntilSuccess( 10, TimeUnit.SECONDS, () -> { final boolean isBuiltImagePresentAfterRunning = isImagePresent(builtImageName.get()); assertThat(isBuiltImagePresentAfterRunning) .as("the built image is not present after running") .isEqualTo(shouldBuiltImageBePresentAfterRunning); return null; } ); Unreliables.retryUntilSuccess( 10, TimeUnit.SECONDS, () -> { final boolean isPulledImagePresentAfterRunning = isImagePresent(pulledImageName.get()); assertThat(isPulledImagePresentAfterRunning) .as("the pulled image is present after running") .isEqualTo(shouldPulledImageBePresentAfterRunning); return null; } ); } private String imageNameForRunningContainer(final String containerNameSuffix) { return DockerClientFactory .instance() .client() .listContainersCmd() .exec() .stream() .filter(it -> Stream.of(it.getNames()).anyMatch(name -> name.endsWith(containerNameSuffix))) .findFirst() .map(Container::getImage) .orElseThrow(IllegalStateException::new); } private boolean isImagePresent(final String imageName) { return DockerClientFactory .instance() .client() .listImagesCmd() .withFilter("reference", Collections.singletonList(imageName)) .exec() .stream() .findFirst() .isPresent(); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/ComposeContainerWithCopyFilesTest.java ================================================ package org.testcontainers.junit; import io.restassured.RestAssured; import org.junit.jupiter.api.Test; import org.testcontainers.containers.ComposeContainer; import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.utility.DockerImageName; import java.io.File; import java.io.IOException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; class ComposeContainerWithCopyFilesTest { @Test void testShouldCopyAllFilesByDefault() throws IOException { try ( ComposeContainer environment = new ComposeContainer( DockerImageName.parse("docker:25.0.5"), new File("src/test/resources/compose-file-copy-inclusions/compose.yml") ) .withExposedService("app", 8080) ) { environment.start(); String response = readStringFromURL(environment); assertThat(response).isEqualTo("MY_ENV_VARIABLE: override"); } } @Test void testWithFileCopyInclusionUsingFilePath() throws IOException { try ( ComposeContainer environment = new ComposeContainer( DockerImageName.parse("docker:25.0.5"), new File("src/test/resources/compose-file-copy-inclusions/compose-root-only.yml") ) .withExposedService("app", 8080) .withCopyFilesInContainer("Dockerfile", "EnvVariableRestEndpoint.java", ".env") ) { environment.start(); String response = readStringFromURL(environment); // The `test/.env` file is not copied, now so we get the original value assertThat(response).isEqualTo("MY_ENV_VARIABLE: original"); } } @Test void testWithFileCopyInclusionUsingDirectoryPath() throws IOException { try ( // composeContainerWithCopyFiles { ComposeContainer environment = new ComposeContainer( DockerImageName.parse("docker:25.0.5"), new File("src/test/resources/compose-file-copy-inclusions/compose-test-only.yml") ) .withExposedService("app", 8080) .withCopyFilesInContainer("Dockerfile", "EnvVariableRestEndpoint.java", "test") // } ) { environment.start(); String response = readStringFromURL(environment); // The test directory (with its contents) is copied, so we get the override assertThat(response).isEqualTo("MY_ENV_VARIABLE: override"); } } @Test void testShouldNotBeAbleToStartIfNeededEnvFileIsNotCopied() { try ( ComposeContainer environment = new ComposeContainer( DockerImageName.parse("docker:25.0.5"), new File("src/test/resources/compose-file-copy-inclusions/compose-test-only.yml") ) .withExposedService("app", 8080) .withCopyFilesInContainer("Dockerfile", "EnvVariableRestEndpoint.java") ) { assertThatExceptionOfType(ContainerLaunchException.class) .isThrownBy(environment::start) .withMessageContaining("Container startup failed for image docker"); } } private static String readStringFromURL(ComposeContainer container) throws IOException { Integer servicePort = container.getServicePort("app-1", 8080); String serviceHost = container.getServiceHost("app-1", 8080); String requestURL = "http://" + serviceHost + ":" + servicePort + "/env"; return RestAssured.get(requestURL).thenReturn().body().asString(); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/ComposeContainerWithOptionsTest.java ================================================ package org.testcontainers.junit; import com.google.common.collect.ImmutableSet; import org.assertj.core.api.Assumptions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.testcontainers.containers.ComposeContainer; import org.testcontainers.utility.CommandLine; import org.testcontainers.utility.DockerImageName; import java.io.File; import java.util.Set; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; /** * Tests the options associated with the docker-compose command. */ class ComposeContainerWithOptionsTest { public static Stream params() { return Stream.of( // Test the happy day case. The compatibility option should be accepted by docker-compose. Arguments.of( new File("src/test/resources/compose-options-test/with-deploy-block.yml"), false, ImmutableSet.of("--compatibility"), false ), // Test with flags absent. Docker compose will warn but continue, ignoring the deploy block. Arguments.of( new File("src/test/resources/compose-options-test/with-deploy-block.yml"), false, ImmutableSet.of(""), false ), // Test with a bad option. Compose will complain. Arguments.of( new File("src/test/resources/compose-options-test/with-deploy-block.yml"), false, ImmutableSet.of("--bad-option"), true ), // Local compose Arguments.of( new File("src/test/resources/compose-options-test/with-deploy-block.yml"), true, ImmutableSet.of("--compatibility"), false ) ); } @ParameterizedTest(name = "docker-compose test [compose file: {0}, local: {1}, options: {2}, expected result: {3}]") @MethodSource("params") void performTest(File composeFile, boolean localMode, Set options, boolean expectError) { ComposeContainer environment; if (localMode) { Assumptions .assumeThat(CommandLine.executableExists(ComposeContainer.COMPOSE_EXECUTABLE)) .as("docker executable exists") .isTrue(); environment = new ComposeContainer(composeFile).withOptions(options.stream().toArray(String[]::new)); } else { environment = new ComposeContainer(DockerImageName.parse("docker:25.0.2"), composeFile) .withOptions(options.stream().toArray(String[]::new)); } try { environment.start(); assertThat(expectError).isFalse(); environment.stop(); } catch (Exception e) { assertThat(expectError).isTrue(); } } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/ComposeContainerWithWaitStrategiesTest.java ================================================ package org.testcontainers.junit; import org.junit.jupiter.api.Test; import org.testcontainers.containers.ComposeContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import java.io.File; import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; class ComposeContainerWithWaitStrategiesTest { private static final int REDIS_PORT = 6379; @Test void testComposeContainerConstructor() { try ( // composeContainerWithCombinedWaitStrategies { ComposeContainer compose = new ComposeContainer( DockerImageName.parse("docker:25.0.5"), 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 void testComposeContainerWaitForPortWithTimeout() { try ( // composeContainerWaitForPortWithTimeout { ComposeContainer compose = new ComposeContainer( DockerImageName.parse("docker:25.0.5"), 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(); } } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/ComposeErrorHandlingTest.java ================================================ package org.testcontainers.junit; import org.junit.jupiter.api.Test; import org.testcontainers.containers.ComposeContainer; import org.testcontainers.utility.DockerImageName; import java.io.File; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; class ComposeErrorHandlingTest { @Test void simpleTest() { assertThat( catchThrowable(() -> { ComposeContainer environment = new ComposeContainer( DockerImageName.parse("docker:25.0.5"), new File("src/test/resources/invalid-compose.yml") ) .withExposedService("something", 123); }) ) .as("starting with an invalid docker-compose file throws an exception") .isInstanceOf(IllegalArgumentException.class); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/ComposePassthroughTest.java ================================================ package org.testcontainers.junit; import org.assertj.core.api.Assumptions; import org.junit.jupiter.api.AutoClose; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.testcontainers.containers.ComposeContainer; import org.testcontainers.containers.ContainerState; import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.TestEnvironment; import java.io.File; import java.util.Arrays; import java.util.Objects; import static org.assertj.core.api.Assertions.assertThat; class ComposePassthroughTest { private final TestWaitStrategy waitStrategy = new TestWaitStrategy(); @BeforeAll public static void checkVersion() { Assumptions.assumeThat(TestEnvironment.dockerApiAtLeast("1.22")).isTrue(); } @AutoClose public ComposeContainer compose = new ComposeContainer( DockerImageName.parse("docker:25.0.5"), new File("src/test/resources/v2-compose-test-passthrough.yml") ) .withEnv("foo", "bar") .withExposedService("alpine-1", 3000, waitStrategy); ComposePassthroughTest() { compose.start(); } @Test void testContainerInstanceProperties() { final ContainerState container = waitStrategy.getContainer(); //check environment variable was set assertThat(Arrays.asList(Objects.requireNonNull(container.getContainerInfo().getConfig().getEnv()))) .as("Environment variable set correctly") .containsOnlyOnce("bar=bar"); //check other container properties assertThat(container.getContainerId()).as("Container id is not null").isNotNull(); assertThat(container.getMappedPort(3000)).as("Port mapped").isNotNull(); assertThat(container.getExposedPorts()).containsExactly(3000); } /* * WaitStrategy is the only class that has access to the DockerComposeServiceInstance reference * Using a custom WaitStrategy to expose the reference for testability */ class TestWaitStrategy extends HostPortWaitStrategy { @SuppressWarnings("unchecked") public ContainerState getContainer() { return this.waitStrategyTarget; } } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/ComposeWaitStrategyTest.java ================================================ package org.testcontainers.junit; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.testcontainers.containers.ComposeContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.containers.wait.strategy.WaitStrategy; import org.testcontainers.utility.DockerImageName; import java.io.File; import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; import static org.assertj.core.api.Assertions.fail; class ComposeWaitStrategyTest { private static final int REDIS_PORT = 6379; private ComposeContainer environment; @BeforeEach public final void setUp() { environment = new ComposeContainer( DockerImageName.parse("docker:25.0.5"), new File("src/test/resources/composev2/compose-test.yml") ); } @AfterEach public final void cleanUp() { environment.stop(); } @Test void testWaitOnListeningPort() { environment.withExposedService("redis-1", REDIS_PORT, Wait.forListeningPort()); try { environment.start(); } catch (RuntimeException e) { fail("Docker compose should start after waiting for listening port with failed with: " + e); } } @Test void testWaitOnMultipleStrategiesPassing() { environment .withExposedService("redis-1", REDIS_PORT, Wait.forListeningPort()) .withExposedService("db-1", 3306, Wait.forLogMessage(".*ready for connections.*\\s", 1)) .withTailChildContainers(true); try { environment.start(); } catch (RuntimeException e) { fail("Docker compose should start after waiting for listening port with failed with: " + e); } } @Test void testWaitingFails() { environment.withExposedService( "redis-1", REDIS_PORT, Wait.forHttp("/test").withStartupTimeout(Duration.ofSeconds(10)) ); assertThat(catchThrowable(() -> environment.start())) .as("waiting on an invalid http path times out") .isInstanceOf(RuntimeException.class); } @Test void testWaitOnOneOfMultipleStrategiesFailing() { environment .withExposedService( "redis-1", REDIS_PORT, Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(10)) ) .waitingFor( "db-1", Wait.forLogMessage(".*test test test.*\\s", 1).withStartupTimeout(Duration.ofSeconds(10)) ) .withTailChildContainers(true); assertThat(catchThrowable(() -> environment.start())) .as("waiting on one failing strategy to time out") .isInstanceOf(RuntimeException.class); } @Test void testWaitingForNonexistentServices() { String existentServiceName = "db-1"; String nonexistentServiceName1 = "some-nonexistent_service-1"; String nonexistentServiceName2 = "some-nonexistent_service-2"; WaitStrategy someWaitStrategy = Mockito.mock(WaitStrategy.class); environment .waitingFor(existentServiceName, someWaitStrategy) .waitingFor(nonexistentServiceName1, someWaitStrategy) .waitingFor(nonexistentServiceName2, someWaitStrategy); Throwable thrownWhenRequestedToWaitForNonexistentService = catchThrowable(environment::start); assertThat(thrownWhenRequestedToWaitForNonexistentService) .isInstanceOf(IllegalStateException.class) .hasMessageContaining(nonexistentServiceName1, nonexistentServiceName2) .hasMessageNotContaining(existentServiceName); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/ComposeWithIdentifierTest.java ================================================ package org.testcontainers.junit; import org.junit.jupiter.api.AutoClose; import org.testcontainers.containers.ComposeContainer; import org.testcontainers.utility.DockerImageName; import java.io.File; class ComposeWithIdentifierTest extends BaseComposeTest { @AutoClose public ComposeContainer environment = new ComposeContainer( DockerImageName.parse("docker:25.0.5"), "TEST", new File("src/test/resources/v2-compose-test.yml") ) .withExposedService("redis-1", REDIS_PORT); ComposeWithIdentifierTest() { environment.start(); } @Override protected ComposeContainer getEnvironment() { return this.environment; } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/ComposeWithNetworkTest.java ================================================ package org.testcontainers.junit; import org.junit.jupiter.api.AutoClose; import org.testcontainers.containers.ComposeContainer; import org.testcontainers.utility.DockerImageName; import java.io.File; class ComposeWithNetworkTest extends BaseComposeTest { @AutoClose public ComposeContainer environment = new ComposeContainer( DockerImageName.parse("docker:25.0.5"), new File("src/test/resources/v2-compose-test-with-network.yml") ) .withExposedService("redis-1", REDIS_PORT); ComposeWithNetworkTest() { environment.start(); } @Override protected ComposeContainer getEnvironment() { return environment; } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/CopyFileToContainerTest.java ================================================ package org.testcontainers.junit; import com.google.common.io.Files; import com.google.common.io.Resources; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.testcontainers.TestImages; import org.testcontainers.containers.BindMode; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.SelinuxContext; import org.testcontainers.utility.MountableFile; import java.io.File; import java.io.IOException; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; class CopyFileToContainerTest { private static String destinationOnHost; private static String directoryInContainer = "/tmp/mappable-resource/"; private static String fileName = "test-resource.txt"; @BeforeEach public void setup() throws IOException { destinationOnHost = File.createTempFile("testcontainers", null).getAbsolutePath(); } @Test void checkFileCopied() throws IOException, InterruptedException { try ( // copyToContainer { GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) .withCommand("sleep", "3000") .withCopyFileToContainer( MountableFile.forClasspathResource("/mappable-resource/"), directoryInContainer ) // } ) { container.start(); String filesList = container.execInContainer("ls", directoryInContainer).getStdout(); assertThat(filesList).as("file list contains the file").contains(fileName); // copyFileFromContainer { container.copyFileFromContainer(directoryInContainer + fileName, destinationOnHost); // } } assertThat(Files.toByteArray(new File(destinationOnHost))) .isEqualTo(Resources.toByteArray(getClass().getResource("/mappable-resource/" + fileName))); } @Test void shouldUseCopyForReadOnlyClasspathResources() throws Exception { try ( GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) .withCommand("sleep", "3000") .withClasspathResourceMapping("/mappable-resource/", directoryInContainer, BindMode.READ_ONLY) ) { container.start(); String filesList = container.execInContainer("ls", "/tmp/mappable-resource").getStdout(); assertThat(filesList).as("file list contains the file").contains(fileName); } } @Test void shouldUseCopyOnlyWithReadOnlyClasspathResources() { String resource = "/test_copy_to_container.txt"; GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) .withClasspathResourceMapping(resource, "/readOnly", BindMode.READ_ONLY) .withClasspathResourceMapping(resource, "/readOnlyShared", BindMode.READ_ONLY, SelinuxContext.SHARED) .withClasspathResourceMapping(resource, "/readWrite", BindMode.READ_WRITE); Map copyMap = container.getCopyToFileContainerPathMap(); assertThat(copyMap).as("uses copy for read-only").containsValue("/readOnly"); assertThat(copyMap).as("uses copy for read-only with Selinux").containsValue("/readOnlyShared"); assertThat(copyMap).as("uses mount for read-write").doesNotContainValue("/readWrite"); } @Test void shouldCreateFoldersStructureWithCopy() throws Exception { String resource = "/test_copy_to_container.txt"; try ( GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) .withCommand("sleep", "3000") .withClasspathResourceMapping(resource, "/a/b/c/file", BindMode.READ_ONLY) ) { container.start(); String filesList = container.execInContainer("ls", "/a/b/c/").getStdout(); assertThat(filesList).as("file list contains the file").contains("file"); } } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/DependenciesTest.java ================================================ package org.testcontainers.junit; import lombok.Getter; import org.junit.jupiter.api.Test; import org.testcontainers.TestImages; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; import org.testcontainers.lifecycle.Startable; import org.testcontainers.lifecycle.Startables; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; class DependenciesTest { @Test void shouldWorkWithSimpleDependency() { InvocationCountingStartable startable = new InvocationCountingStartable(); try ( GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) .dependsOn(startable) ) { container.start(); } assertThat(startable.getStartInvocationCount().intValue()).as("Started once").isEqualTo(1); assertThat(startable.getStopInvocationCount().intValue()).as("Does not trigger .stop()").isZero(); } @Test void shouldWorkWithMultipleDependencies() { InvocationCountingStartable startable1 = new InvocationCountingStartable(); InvocationCountingStartable startable2 = new InvocationCountingStartable(); try ( GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) .dependsOn(startable1, startable2) ) { container.start(); } assertThat(startable1.getStartInvocationCount().intValue()).as("Startable1 started once").isEqualTo(1); assertThat(startable2.getStartInvocationCount().intValue()).as("Startable2 started once").isEqualTo(1); } @Test void shouldStartEveryTime() { InvocationCountingStartable startable = new InvocationCountingStartable(); try ( GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) .dependsOn(startable) ) { container.start(); container.stop(); container.start(); container.stop(); container.start(); } assertThat(startable.getStartInvocationCount().intValue()).as("Started multiple times").isEqualTo(3); assertThat(startable.getStopInvocationCount().intValue()).as("Does not trigger .stop()").isZero(); } @Test void shouldStartTransitiveDependencies() { InvocationCountingStartable transitiveOfTransitiveStartable = new InvocationCountingStartable(); InvocationCountingStartable transitiveStartable = new InvocationCountingStartable(); transitiveStartable.getDependencies().add(transitiveOfTransitiveStartable); InvocationCountingStartable startable = new InvocationCountingStartable(); startable.getDependencies().add(transitiveStartable); try ( GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) .dependsOn(startable) ) { container.start(); container.stop(); } assertThat(startable.getStartInvocationCount().intValue()).as("Root started").isEqualTo(1); assertThat(transitiveStartable.getStartInvocationCount().intValue()).as("Transitive started").isEqualTo(1); assertThat(transitiveOfTransitiveStartable.getStartInvocationCount().intValue()) .as("Transitive of transitive started") .isEqualTo(1); } @Test void shouldHandleDiamondDependencies() throws Exception { InvocationCountingStartable a = new InvocationCountingStartable(); InvocationCountingStartable b = new InvocationCountingStartable(); InvocationCountingStartable c = new InvocationCountingStartable(); InvocationCountingStartable d = new InvocationCountingStartable(); // / b \ // a d // \ c / b.getDependencies().add(a); c.getDependencies().add(a); d.getDependencies().add(b); d.getDependencies().add(c); Startables.deepStart(Stream.of(d)).get(1, TimeUnit.SECONDS); assertThat(a.getStartInvocationCount().intValue()).as("A started").isEqualTo(1); assertThat(b.getStartInvocationCount().intValue()).as("B started").isEqualTo(1); assertThat(c.getStartInvocationCount().intValue()).as("C started").isEqualTo(1); assertThat(d.getStartInvocationCount().intValue()).as("D started").isEqualTo(1); } @Test void shouldHandleParallelStream() throws Exception { List startables = Stream .generate(InvocationCountingStartable::new) .limit(10) .collect(Collectors.toList()); for (int i = 1; i < startables.size(); i++) { startables.get(0).getDependencies().add(startables.get(i)); } Startables.deepStart(startables.parallelStream()).get(1, TimeUnit.SECONDS); } private static class InvocationCountingStartable implements Startable { @Getter Set dependencies = new HashSet<>(); @Getter AtomicLong startInvocationCount = new AtomicLong(0); @Getter AtomicLong stopInvocationCount = new AtomicLong(0); @Override public void start() { startInvocationCount.getAndIncrement(); } @Override public void stop() { stopInvocationCount.getAndIncrement(); } } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/DockerComposeContainerPortViaEnvTest.java ================================================ package org.testcontainers.junit; import org.junit.jupiter.api.AutoClose; import org.junit.jupiter.api.Disabled; import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.utility.DockerImageName; import java.io.File; @Disabled class DockerComposeContainerPortViaEnvTest extends BaseDockerComposeTest { @AutoClose public DockerComposeContainer environment = new DockerComposeContainer( DockerImageName.parse("docker/compose:1.29.2"), new File("src/test/resources/v2-compose-test-port-via-env.yml") ) .withExposedService("redis_1", REDIS_PORT) .withEnv("REDIS_PORT", String.valueOf(REDIS_PORT)); DockerComposeContainerPortViaEnvTest() { environment.start(); } @Override protected DockerComposeContainer getEnvironment() { return environment; } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/DockerComposeContainerScalingTest.java ================================================ package org.testcontainers.junit; import org.assertj.core.api.Assumptions; import org.junit.jupiter.api.AutoClose; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.TestEnvironment; import redis.clients.jedis.Jedis; import java.io.File; import static org.assertj.core.api.Assertions.assertThat; /** * Created by rnorth on 08/08/2015. */ class DockerComposeContainerScalingTest { private static final int REDIS_PORT = 6379; private Jedis[] clients = new Jedis[3]; @BeforeAll public static void checkVersion() { Assumptions.assumeThat(TestEnvironment.dockerApiAtLeast("1.22")).isTrue(); } @AutoClose public DockerComposeContainer environment = new DockerComposeContainer( DockerImageName.parse("docker/compose:1.29.2"), new File("src/test/resources/scaled-compose-test.yml") ) .withScaledService("redis", 3) .withExposedService("redis", REDIS_PORT) // implicit '_1' .withExposedService("redis_2", REDIS_PORT) // explicit service index .withExposedService("redis", 3, REDIS_PORT); // explicit service index via parameter @BeforeEach public void setupClients() { this.environment.start(); for (int i = 0; i < 3; i++) { String name = String.format("redis_%d", i + 1); clients[i] = new Jedis(environment.getServiceHost(name, REDIS_PORT), environment.getServicePort(name, REDIS_PORT)); } } @Test void simpleTest() { for (int i = 0; i < 3; i++) { clients[i].incr("somekey"); assertThat(clients[i].get("somekey")).as("Each redis instance is separate").isEqualTo("1"); } } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/DockerComposeContainerTest.java ================================================ package org.testcontainers.junit; import org.junit.jupiter.api.AutoClose; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.testcontainers.containers.ContainerState; import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.utility.DockerImageName; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.Collections; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; /** * Created by rnorth on 08/08/2015. */ @Disabled class DockerComposeContainerTest extends BaseDockerComposeTest { @AutoClose public DockerComposeContainer environment = new DockerComposeContainer( DockerImageName.parse("docker/compose:1.29.2"), new File("src/test/resources/compose-test.yml") ) .withExposedService("redis_1", REDIS_PORT) .withExposedService("db_1", 3306); DockerComposeContainerTest() { environment.start(); } @Override protected DockerComposeContainer getEnvironment() { return environment; } @Test void testGetServicePort() { int serviceWithInstancePort = environment.getServicePort("redis_1", REDIS_PORT); 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); } @Test void shouldRetrieveContainerByServiceName() { String existingServiceName = "db_1"; Optional result = environment.getContainerByServiceName(existingServiceName); assertThat(result) .as(String.format("Container should be found by service name %s", existingServiceName)) .isPresent(); assertThat(Collections.singletonList(3306)) .as("Mapped port for result container was wrong, probably wrong container found") .isEqualTo(result.get().getExposedPorts()); } @Test void shouldRetrieveContainerByServiceNameWithoutNumberedSuffix() { String existingServiceName = "db"; Optional result = environment.getContainerByServiceName(existingServiceName); assertThat(result) .as(String.format("Container should be found by service name %s", existingServiceName)) .isPresent(); assertThat(result.get().getExposedPorts()) .as("Mapped port for result container was wrong, perhaps wrong container was found") .isEqualTo(Collections.singletonList(3306)); } @Test void shouldReturnEmptyResultOnNoneExistingService() { String notExistingServiceName = "db_256"; Optional result = environment.getContainerByServiceName(notExistingServiceName); assertThat(result) .as(String.format("No container should be found under service name %s", notExistingServiceName)) .isNotPresent(); } @Test void shouldCreateContainerWhenFileNotPrefixedWithPath() throws IOException { String validYaml = "version: '2.2'\n" + "services:\n" + " http:\n" + " build: .\n" + " image: python:latest\n" + " ports:\n" + " - 8080:8080"; File filePathNotStartWithDotSlash = new File("docker-compose-test.yml"); filePathNotStartWithDotSlash.createNewFile(); filePathNotStartWithDotSlash.deleteOnExit(); Files.write(filePathNotStartWithDotSlash.toPath(), validYaml.getBytes(StandardCharsets.UTF_8)); final DockerComposeContainer dockerComposeContainer = new DockerComposeContainer<>( DockerImageName.parse("docker/compose:debian-1.29.2"), filePathNotStartWithDotSlash ); assertThat(dockerComposeContainer).as("Container could not be created using docker compose file").isNotNull(); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/DockerComposeContainerVolumeRemovalTest.java ================================================ package org.testcontainers.junit; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.utility.DockerImageName; import java.io.File; import java.util.LinkedHashSet; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @Disabled class DockerComposeContainerVolumeRemovalTest { public static Object[][] params() { return new Object[][] { { true, false }, { false, true } }; } @ParameterizedTest @MethodSource("params") void performTest(boolean removeVolumes, boolean shouldVolumesBePresentAfterRunning) { final File composeFile = new File("src/test/resources/compose-test.yml"); final AtomicReference volumeName = new AtomicReference<>(""); try ( DockerComposeContainer environment = new DockerComposeContainer<>( DockerImageName.parse("docker/compose:1.29.2"), composeFile ) .withExposedService("redis", 6379) .withRemoveVolumes(removeVolumes) .withRemoveImages(DockerComposeContainer.RemoveImages.ALL) ) { environment.start(); volumeName.set(volumeNameForRunningContainer("_redis_1")); final boolean isVolumePresentWhileRunning = isVolumePresent(volumeName.get()); assertThat(isVolumePresentWhileRunning).as("the container volume is present while running").isEqualTo(true); } await() .untilAsserted(() -> { final boolean isVolumePresentAfterRunning = isVolumePresent(volumeName.get()); assertThat(isVolumePresentAfterRunning) .as("the container volume is present after running") .isEqualTo(shouldVolumesBePresentAfterRunning); }); } private String volumeNameForRunningContainer(final String containerNameSuffix) { return DockerClientFactory .instance() .client() .listContainersCmd() .exec() .stream() .filter(it -> Stream.of(it.getNames()).anyMatch(name -> name.endsWith(containerNameSuffix))) .findFirst() .map(container -> container.getMounts().get(0).getName()) .orElseThrow(IllegalStateException::new); } private boolean isVolumePresent(final String volumeName) { Set nameFilter = new LinkedHashSet<>(1); nameFilter.add(volumeName); return DockerClientFactory .instance() .client() .listVolumesCmd() .withFilter("name", nameFilter) .exec() .getVolumes() .stream() .findFirst() .isPresent(); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/DockerComposeContainerWithBuildTest.java ================================================ package org.testcontainers.junit; import com.github.dockerjava.api.model.Container; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.rnorth.ducttape.unreliables.Unreliables; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.utility.DockerImageName; import java.io.File; import java.util.Collections; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; class DockerComposeContainerWithBuildTest { public static Stream params() { return Stream.of( Arguments.of(null, true, true), Arguments.of(DockerComposeContainer.RemoveImages.LOCAL, false, true), Arguments.of(DockerComposeContainer.RemoveImages.ALL, false, false) ); } @ParameterizedTest @MethodSource("params") void performTest( DockerComposeContainer.RemoveImages removeMode, boolean shouldBuiltImageBePresentAfterRunning, boolean shouldPulledImageBePresentAfterRunning ) { final File composeFile = new File("src/test/resources/compose-build-test/docker-compose.yml"); final AtomicReference builtImageName = new AtomicReference<>(""); final AtomicReference pulledImageName = new AtomicReference<>(""); try ( DockerComposeContainer environment = new DockerComposeContainer<>( DockerImageName.parse("docker/compose:1.29.2"), composeFile ) .withExposedService("customredis", 6379) .withBuild(true) .withRemoveImages(removeMode) ) { environment.start(); builtImageName.set(imageNameForRunningContainer("_customredis_1")); final boolean isBuiltImagePresentWhileRunning = isImagePresent(builtImageName.get()); assertThat(isBuiltImagePresentWhileRunning).as("the built image is present while running").isEqualTo(true); pulledImageName.set(imageNameForRunningContainer("_normalredis_1")); final boolean isPulledImagePresentWhileRunning = isImagePresent(pulledImageName.get()); assertThat(isPulledImagePresentWhileRunning) .as("the pulled image is present while running") .isEqualTo(true); } Unreliables.retryUntilSuccess( 10, TimeUnit.SECONDS, () -> { final boolean isBuiltImagePresentAfterRunning = isImagePresent(builtImageName.get()); assertThat(isBuiltImagePresentAfterRunning) .as("the built image is not present after running") .isEqualTo(shouldBuiltImageBePresentAfterRunning); return null; } ); Unreliables.retryUntilSuccess( 10, TimeUnit.SECONDS, () -> { final boolean isPulledImagePresentAfterRunning = isImagePresent(pulledImageName.get()); assertThat(isPulledImagePresentAfterRunning) .as("the pulled image is present after running") .isEqualTo(shouldPulledImageBePresentAfterRunning); return null; } ); } private String imageNameForRunningContainer(final String containerNameSuffix) { return DockerClientFactory .instance() .client() .listContainersCmd() .exec() .stream() .filter(it -> Stream.of(it.getNames()).anyMatch(name -> name.endsWith(containerNameSuffix))) .findFirst() .map(Container::getImage) .orElseThrow(IllegalStateException::new); } private boolean isImagePresent(final String imageName) { return DockerClientFactory .instance() .client() .listImagesCmd() .withFilter("reference", Collections.singletonList(imageName)) .exec() .stream() .findFirst() .isPresent(); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/DockerComposeContainerWithCopyFilesTest.java ================================================ package org.testcontainers.junit; import io.restassured.RestAssured; import org.junit.jupiter.api.Test; import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.utility.DockerImageName; import java.io.File; import java.io.IOException; import static org.assertj.core.api.Assertions.assertThat; class DockerComposeContainerWithCopyFilesTest { @Test void testShouldCopyAllFilesByDefault() throws IOException { try ( DockerComposeContainer environment = new DockerComposeContainer( DockerImageName.parse("docker/compose:1.29.2"), new File("src/test/resources/compose-file-copy-inclusions/compose.yml") ) .withExposedService("app", 8080) ) { environment.start(); String response = readStringFromURL(environment); assertThat(response).isEqualTo("MY_ENV_VARIABLE: override"); } } @Test void testWithFileCopyInclusionUsingFilePath() throws IOException { try ( DockerComposeContainer environment = new DockerComposeContainer( DockerImageName.parse("docker/compose:1.29.2"), new File("src/test/resources/compose-file-copy-inclusions/compose-root-only.yml") ) .withExposedService("app", 8080) .withCopyFilesInContainer("Dockerfile", "EnvVariableRestEndpoint.java", ".env") ) { environment.start(); String response = readStringFromURL(environment); // The `test/.env` file is not copied, now so we get the original value assertThat(response).isEqualTo("MY_ENV_VARIABLE: original"); } } @Test void testWithFileCopyInclusionUsingDirectoryPath() throws IOException { try ( DockerComposeContainer environment = new DockerComposeContainer( DockerImageName.parse("docker/compose:1.29.2"), new File("src/test/resources/compose-file-copy-inclusions/compose-test-only.yml") ) .withExposedService("app", 8080) .withCopyFilesInContainer("Dockerfile", "EnvVariableRestEndpoint.java", "test") ) { environment.start(); String response = readStringFromURL(environment); // The test directory (with its contents) is copied, so we get the override assertThat(response).isEqualTo("MY_ENV_VARIABLE: override"); } } private static String readStringFromURL(DockerComposeContainer container) throws IOException { Integer servicePort = container.getServicePort("app_1", 8080); String serviceHost = container.getServiceHost("app_1", 8080); String requestURL = "http://" + serviceHost + ":" + servicePort + "/env"; return RestAssured.get(requestURL).thenReturn().body().asString(); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/DockerComposeContainerWithOptionsTest.java ================================================ package org.testcontainers.junit; import com.google.common.collect.ImmutableSet; import org.assertj.core.api.Assumptions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.utility.CommandLine; import org.testcontainers.utility.DockerImageName; import java.io.File; import java.util.Set; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; /** * Tests the options associated with the docker-compose command. */ class DockerComposeContainerWithOptionsTest { public static Stream params() { return Stream.of( // Test the happy day case. The compatibility option should be accepted by docker-compose. Arguments.of( new File("src/test/resources/compose-options-test/with-deploy-block.yml"), false, ImmutableSet.of("--compatibility"), false ), // Test with flags absent. Docker compose will warn but continue, ignoring the deploy block. Arguments.of( new File("src/test/resources/compose-options-test/with-deploy-block.yml"), false, ImmutableSet.of(""), false ), // Test with a bad option. Compose will complain. Arguments.of( new File("src/test/resources/compose-options-test/with-deploy-block.yml"), false, ImmutableSet.of("--bad-option"), true ), // Local compose Arguments.of( new File("src/test/resources/compose-options-test/with-deploy-block.yml"), true, ImmutableSet.of("--compatibility"), false ) ); } @ParameterizedTest(name = "docker-compose test [compose file: {0}, local: {1}, options: {2}, expected result: {3}]") @MethodSource("params") void performTest(File composeFile, boolean localMode, Set options, boolean expectError) { DockerComposeContainer environment; if (localMode) { Assumptions .assumeThat(CommandLine.executableExists(DockerComposeContainer.COMPOSE_EXECUTABLE)) .as("docker-compose executable exists") .isTrue(); Assumptions .assumeThat(CommandLine.runShellCommand("docker-compose", "--version")) .doesNotStartWith("Docker Compose version v2"); environment = new DockerComposeContainer<>(composeFile).withOptions(options.stream().toArray(String[]::new)); } else { environment = new DockerComposeContainer<>(DockerImageName.parse("docker/compose:debian-1.29.2"), composeFile) .withOptions(options.stream().toArray(String[]::new)); } try { environment.start(); assertThat(expectError).isEqualTo(false); environment.stop(); } catch (Exception e) { assertThat(expectError).isEqualTo(true); } } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/DockerComposeErrorHandlingTest.java ================================================ package org.testcontainers.junit; import org.junit.jupiter.api.Test; import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.utility.DockerImageName; import java.io.File; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; class DockerComposeErrorHandlingTest { @Test void simpleTest() { assertThat( catchThrowable(() -> { DockerComposeContainer environment = new DockerComposeContainer( DockerImageName.parse("docker/compose:1.29.2"), new File("src/test/resources/invalid-compose.yml") ) .withExposedService("something", 123); }) ) .as("starting with an invalid docker-compose file throws an exception") .isInstanceOf(IllegalArgumentException.class); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/DockerComposeLocalImageTest.java ================================================ package org.testcontainers.junit; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.core.command.PullImageResultCallback; import org.junit.jupiter.api.Test; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.utility.DockerImageName; import java.io.File; class DockerComposeLocalImageTest { @Test void usesLocalImageEvenWhenPullFails() throws InterruptedException { tagImage("redis:6-alpine", "redis-local", "latest"); DockerComposeContainer composeContainer = new DockerComposeContainer( DockerImageName.parse("docker/compose:1.29.2"), new File("src/test/resources/local-compose-test.yml") ) .withExposedService("redis", 6379); composeContainer.start(); } private void tagImage(String sourceImage, String targetImage, String targetTag) throws InterruptedException { DockerClient client = DockerClientFactory.instance().client(); client.pullImageCmd(sourceImage).exec(new PullImageResultCallback()).awaitCompletion(); client.tagImageCmd(sourceImage, targetImage, targetTag).exec(); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/DockerComposeLogConsumerTest.java ================================================ package org.testcontainers.junit; import org.junit.jupiter.api.Test; import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.containers.output.OutputFrame.OutputType; import org.testcontainers.containers.output.WaitingConsumer; import org.testcontainers.utility.DockerImageName; import java.io.File; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; class DockerComposeLogConsumerTest { @Test void testLogConsumer() throws TimeoutException { WaitingConsumer logConsumer = new WaitingConsumer(); DockerComposeContainer environment = new DockerComposeContainer( DockerImageName.parse("docker/compose:1.29.2"), new File("src/test/resources/v2-compose-test.yml") ) .withExposedService("redis_1", 6379) .withLogConsumer("redis_1", logConsumer); try { environment.start(); logConsumer.waitUntil( frame -> { return ( frame.getType() == OutputType.STDOUT && frame.getUtf8String().contains("Ready to accept connections") ); }, 5, TimeUnit.SECONDS ); } finally { environment.stop(); } } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/DockerComposePassthroughTest.java ================================================ package org.testcontainers.junit; import org.assertj.core.api.Assumptions; import org.junit.jupiter.api.Test; import org.testcontainers.containers.ContainerState; import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.TestEnvironment; import java.io.File; import java.util.Arrays; import java.util.Objects; import static org.assertj.core.api.Assertions.assertThat; /** * Created by rnorth on 11/06/2016. */ class DockerComposePassthroughTest { @Test void testContainerInstanceProperties() { Assumptions.assumeThat(TestEnvironment.dockerApiAtLeast("1.22")).isTrue(); TestWaitStrategy waitStrategy = new TestWaitStrategy(); try ( DockerComposeContainer compose = new DockerComposeContainer( DockerImageName.parse("docker/compose:1.29.2"), new File("src/test/resources/v2-compose-test-passthrough.yml") ) .withEnv("foo", "bar") .withExposedService("alpine_1", 3000, waitStrategy) ) { compose.start(); final ContainerState container = waitStrategy.getContainer(); //check environment variable was set assertThat(Arrays.asList(Objects.requireNonNull(container.getContainerInfo().getConfig().getEnv()))) .as("Environment variable set correctly") .containsOnlyOnce("bar=bar"); //check other container properties assertThat(container.getContainerId()).as("Container id is not null").isNotNull(); assertThat(container.getMappedPort(3000)).as("Port mapped").isNotNull(); assertThat(container.getExposedPorts()).containsExactly(3000); } } /* * WaitStrategy is the only class that has access to the DockerComposeServiceInstance reference * Using a custom WaitStrategy to expose the reference for testability */ class TestWaitStrategy extends HostPortWaitStrategy { @SuppressWarnings("unchecked") public ContainerState getContainer() { return this.waitStrategyTarget; } } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/DockerComposeServiceTest.java ================================================ package org.testcontainers.junit; import org.junit.jupiter.api.AutoClose; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.utility.DockerImageName; import java.io.File; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @Disabled class DockerComposeServiceTest extends BaseDockerComposeTest { @AutoClose public DockerComposeContainer environment = new DockerComposeContainer( DockerImageName.parse("docker/compose:1.29.2"), new File("src/test/resources/compose-test.yml") ) .withServices("redis") .withExposedService("redis_1", REDIS_PORT); DockerComposeServiceTest() { environment.start(); } @Override protected DockerComposeContainer getEnvironment() { return environment; } @Test void testDbIsNotStarting() { assertThatThrownBy(() -> { environment.getServicePort("db_1", 10001); }) .isInstanceOf(IllegalArgumentException.class); } @Test void testRedisIsStarting() { assertThat(environment.getServicePort("redis_1", REDIS_PORT)).as("Redis server started").isNotNull(); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/DockerComposeV2FormatTest.java ================================================ package org.testcontainers.junit; import org.junit.jupiter.api.AutoClose; import org.junit.jupiter.api.Disabled; import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.utility.DockerImageName; import java.io.File; /** * Created by rnorth on 21/05/2016. */ @Disabled class DockerComposeV2FormatTest extends BaseDockerComposeTest { @AutoClose public DockerComposeContainer environment = new DockerComposeContainer( DockerImageName.parse("docker/compose:1.29.2"), new File("src/test/resources/v2-compose-test.yml") ) .withExposedService("redis_1", REDIS_PORT); DockerComposeV2FormatTest() { environment.start(); } @Override protected DockerComposeContainer getEnvironment() { return environment; } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/DockerComposeV2FormatWithIdentifierTest.java ================================================ package org.testcontainers.junit; import org.junit.jupiter.api.AutoClose; import org.junit.jupiter.api.Disabled; import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.utility.DockerImageName; import java.io.File; @Disabled class DockerComposeV2FormatWithIdentifierTest extends BaseDockerComposeTest { @AutoClose public DockerComposeContainer environment = new DockerComposeContainer( DockerImageName.parse("docker/compose:1.29.2"), "TEST", new File("src/test/resources/v2-compose-test.yml") ) .withExposedService("redis_1", REDIS_PORT); DockerComposeV2FormatWithIdentifierTest() { environment.start(); } @Override protected DockerComposeContainer getEnvironment() { return this.environment; } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/DockerComposeV2WithNetworkTest.java ================================================ package org.testcontainers.junit; import org.junit.jupiter.api.AutoClose; import org.junit.jupiter.api.Disabled; import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.utility.DockerImageName; import java.io.File; @Disabled class DockerComposeV2WithNetworkTest extends BaseDockerComposeTest { @AutoClose public DockerComposeContainer environment = new DockerComposeContainer( DockerImageName.parse("docker/compose:1.29.2"), new File("src/test/resources/v2-compose-test-with-network.yml") ) .withExposedService("redis_1", REDIS_PORT); DockerComposeV2WithNetworkTest() { environment.start(); } @Override protected DockerComposeContainer getEnvironment() { return environment; } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/DockerComposeWaitStrategyTest.java ================================================ package org.testcontainers.junit; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.containers.wait.strategy.WaitStrategy; import org.testcontainers.utility.DockerImageName; import java.io.File; import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; import static org.assertj.core.api.Assertions.fail; class DockerComposeWaitStrategyTest { private static final int REDIS_PORT = 6379; private DockerComposeContainer environment; @BeforeEach public final void setUp() { environment = new DockerComposeContainer<>( DockerImageName.parse("docker/compose:1.29.2"), new File("src/test/resources/compose-test.yml") ); } @AfterEach public final void cleanUp() { environment.stop(); } @Test void testWaitOnListeningPort() { environment.withExposedService("redis_1", REDIS_PORT, Wait.forListeningPort()); try { environment.start(); } catch (RuntimeException e) { fail("Docker compose should start after waiting for listening port with failed with: " + e); } } @Test void testWaitOnMultipleStrategiesPassing() { environment .withExposedService("redis_1", REDIS_PORT, Wait.forListeningPort()) .withExposedService("db_1", 3306, Wait.forLogMessage(".*ready for connections.*\\s", 1)) .withTailChildContainers(true); try { environment.start(); } catch (RuntimeException e) { fail("Docker compose should start after waiting for listening port with failed with: " + e); } } @Test void testWaitingFails() { environment.withExposedService( "redis_1", REDIS_PORT, Wait.forHttp("/test").withStartupTimeout(Duration.ofSeconds(10)) ); assertThat(catchThrowable(() -> environment.start())) .as("waiting on an invalid http path times out") .isInstanceOf(RuntimeException.class); } @Test void testWaitOnOneOfMultipleStrategiesFailing() { environment .withExposedService( "redis_1", REDIS_PORT, Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(10)) ) .waitingFor( "db_1", Wait.forLogMessage(".*test test test.*\\s", 1).withStartupTimeout(Duration.ofSeconds(10)) ) .withTailChildContainers(true); assertThat(catchThrowable(() -> environment.start())) .as("waiting on one failing strategy to time out") .isInstanceOf(RuntimeException.class); } @Test void testWaitingForNonexistentServices() { String existentServiceName = "db_1"; String nonexistentServiceName1 = "some_nonexistent_service_1"; String nonexistentServiceName2 = "some_nonexistent_service_2"; WaitStrategy someWaitStrategy = Mockito.mock(WaitStrategy.class); environment .waitingFor(existentServiceName, someWaitStrategy) .waitingFor(nonexistentServiceName1, someWaitStrategy) .waitingFor(nonexistentServiceName2, someWaitStrategy); Throwable thrownWhenRequestedToWaitForNonexistentService = catchThrowable(environment::start); assertThat(thrownWhenRequestedToWaitForNonexistentService) .isInstanceOf(IllegalStateException.class) .hasMessageContaining(nonexistentServiceName1, nonexistentServiceName2) .hasMessageNotContaining(existentServiceName); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/DockerNetworkModeTest.java ================================================ package org.testcontainers.junit; import com.github.dockerjava.api.model.NetworkSettings; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.testcontainers.TestImages; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; import static org.assertj.core.api.Assertions.assertThat; /** * Simple tests of named network modes - more may be possible, but may not be reproducible * without other setup steps. */ @Slf4j class DockerNetworkModeTest { @Test void testNoNetworkContainer() { try ( GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) .withCommand("true") .withNetworkMode("none") ) { container.start(); NetworkSettings networkSettings = container.getContainerInfo().getNetworkSettings(); assertThat(networkSettings.getNetworks()).as("only one network is set").hasSize(1); assertThat(networkSettings.getNetworks()).as("network is 'none'").containsKey("none"); } } @Test void testHostNetworkContainer() { try ( GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) .withCommand("true") .withNetworkMode("host") ) { container.start(); NetworkSettings networkSettings = container.getContainerInfo().getNetworkSettings(); assertThat(networkSettings.getNetworks()).as("only one network is set").hasSize(1); assertThat(networkSettings.getNetworks()).as("network is 'host'").containsKey("host"); } } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/DockerfileContainerTest.java ================================================ package org.testcontainers.junit; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.junit.jupiter.api.AutoClose; import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.images.builder.ImageFromDockerfile; import java.io.IOException; import static org.assertj.core.api.Assertions.assertThat; /** * Simple test case / demonstration of creating a fresh container image from a Dockerfile DSL */ class DockerfileContainerTest { @AutoClose public GenericContainer dslContainer = new GenericContainer( new ImageFromDockerfile("tcdockerfile/nginx", false) .withDockerfileFromBuilder(builder -> { builder .from("alpine:3.2") // .run("apk add --update nginx") .cmd("nginx", "-g", "daemon off;") .build(); }) ) .withExposedPorts(80); @Test void simpleDslTest() throws IOException { dslContainer.start(); String address = String.format("http://%s:%s", dslContainer.getHost(), dslContainer.getMappedPort(80)); CloseableHttpClient httpClient = HttpClientBuilder.create().build(); HttpGet get = new HttpGet(address); try (CloseableHttpResponse response = httpClient.execute(get)) { assertThat(response.getStatusLine().getStatusCode()) .as("A container built from a dockerfile can run nginx as expected, and returns a good status code") .isEqualTo(200); assertThat(response.getHeaders("Server")[0].getValue()) .as( "A container built from a dockerfile can run nginx as expected, and returns an expected Server header" ) .contains("nginx"); } } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/DockerfileTest.java ================================================ package org.testcontainers.junit; import com.github.dockerjava.api.command.BuildImageCmd; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.output.OutputFrame; import org.testcontainers.containers.output.WaitingConsumer; import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; import org.testcontainers.images.builder.ImageFromDockerfile; import org.testcontainers.images.builder.Transferable; import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; class DockerfileTest { private static final Logger LOGGER = LoggerFactory.getLogger(DockerfileTest.class); @Test void simpleDockerfileWorks() { ImageFromDockerfile image = new ImageFromDockerfile() .withFileFromString("folder/someFile.txt", "hello") .withFileFromClasspath("test.txt", "mappable-resource/test-resource.txt") .withFileFromClasspath("Dockerfile", "mappable-dockerfile/Dockerfile"); verifyImage(image); } @Test void customizableImage() { ImageFromDockerfile image = new ImageFromDockerfile() { @Override protected void configure(BuildImageCmd buildImageCmd) { super.configure(buildImageCmd); List dockerfile = Arrays.asList( "FROM alpine:3.17", "RUN echo 'hello from Docker build process'", "CMD yes" ); withFileFromString("Dockerfile", String.join("\n", dockerfile)); buildImageCmd.withNoCache(true); } }; verifyImage(image); } @Test void dockerfileBuilderWorks() { ImageFromDockerfile image = new ImageFromDockerfile() .withFileFromClasspath("test.txt", "mappable-resource/test-resource.txt") .withFileFromString("folder/someFile.txt", "hello") .withDockerfileFromBuilder(builder -> { builder .from("alpine:3.17") .workDir("/app") .add("test.txt", "test file.txt") .run("ls", "-la", "/app/test file.txt") .copy("folder/someFile.txt", "/someFile.txt") .expose(80, 8080) .cmd("while true; do cat /someFile.txt | nc -l -p 80; done"); }); verifyImage(image); } @Test void filePermissions() throws TimeoutException { WaitingConsumer consumer = new WaitingConsumer(); ImageFromDockerfile image = new ImageFromDockerfile() .withFileFromTransferable( "/someFile.txt", new Transferable() { @Override public long getSize() { return 0; } @Override public byte[] getBytes() { return new byte[0]; } @Override public String getDescription() { return "test file"; } @Override public int getFileMode() { return 0123; } } ) .withDockerfileFromBuilder(builder -> { builder .from("alpine:3.17") // .copy("someFile.txt", "/someFile.txt") .cmd("stat -c \"%a\" /someFile.txt"); }); GenericContainer container = new GenericContainer(image) .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) .withLogConsumer(consumer); try { container.start(); consumer.waitUntil( frame -> frame.getType() == OutputFrame.OutputType.STDOUT && frame.getUtf8String().contains("123"), 5, TimeUnit.SECONDS ); } finally { container.stop(); } } protected void verifyImage(ImageFromDockerfile image) { GenericContainer container = new GenericContainer(image); try { container.start(); } finally { container.stop(); } } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/ExecInContainerTest.java ================================================ package org.testcontainers.junit; import org.assertj.core.api.Assumptions; import org.junit.jupiter.api.AutoClose; import org.junit.jupiter.api.Test; import org.testcontainers.TestImages; import org.testcontainers.containers.ExecConfig; import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.TestEnvironment; import java.util.Collections; import static org.assertj.core.api.Assertions.assertThat; class ExecInContainerTest { @AutoClose public static GenericContainer redis = new GenericContainer<>(TestImages.REDIS_IMAGE).withExposedPorts(6379); static { redis.start(); } @Test void shouldExecuteCommand() throws Exception { // The older "lxc" execution driver doesn't support "exec". At the time of writing (2016/03/29), // that's the case for CircleCI. // Once they resolve the issue, this clause can be removed. Assumptions.assumeThat(TestEnvironment.dockerExecutionDriverSupportsExec()).isTrue(); final GenericContainer.ExecResult result = redis.execInContainer("redis-cli", "role"); assertThat(result.getStdout()) .as("Output for \"redis-cli role\" command should start with \"master\"") .startsWith("master"); assertThat(result.getStderr()).as("Stderr for \"redis-cli role\" command should be empty").isEmpty(); // We expect to reach this point for modern Docker versions. } @Test void shouldExecuteCommandWithUser() throws Exception { // The older "lxc" execution driver doesn't support "exec". At the time of writing (2016/03/29), // that's the case for CircleCI. // Once they resolve the issue, this clause can be removed. Assumptions.assumeThat(TestEnvironment.dockerExecutionDriverSupportsExec()).isTrue(); final GenericContainer.ExecResult result = redis.execInContainerWithUser("redis", "whoami"); assertThat(result.getStdout()) .as("Output for \"whoami\" command should start with \"redis\"") .startsWith("redis"); assertThat(result.getStderr()).as("Stderr for \"whoami\" command should be empty").isEmpty(); // We expect to reach this point for modern Docker versions. } @Test void shouldExecuteCommandWithWorkdir() throws Exception { Assumptions.assumeThat(TestEnvironment.dockerExecutionDriverSupportsExec()).isTrue(); final GenericContainer.ExecResult result = redis.execInContainer( ExecConfig.builder().workDir("/opt").command(new String[] { "pwd" }).build() ); assertThat(result.getStdout()).startsWith("/opt"); } @Test void shouldExecuteCommandWithEnvVars() throws Exception { Assumptions.assumeThat(TestEnvironment.dockerExecutionDriverSupportsExec()).isTrue(); final GenericContainer.ExecResult result = redis.execInContainer( ExecConfig .builder() .envVars(Collections.singletonMap("TESTCONTAINERS", "JAVA")) .command(new String[] { "env" }) .build() ); assertThat(result.getStdout()).contains("TESTCONTAINERS=JAVA"); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/FileOperationsTest.java ================================================ package org.testcontainers.junit; import com.github.dockerjava.api.exception.NotFoundException; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.CountingOutputStream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.testcontainers.TestImages; import org.testcontainers.containers.Container; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; import org.testcontainers.utility.MountableFile; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Path; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class FileOperationsTest { @TempDir public Path temporaryFolder; @Test void copyFileToContainerFileTest() throws Exception { try ( GenericContainer alpineCopyToContainer = new GenericContainer(TestImages.ALPINE_IMAGE) // .withCommand("top") ) { alpineCopyToContainer.start(); final MountableFile mountableFile = MountableFile.forClasspathResource("test_copy_to_container.txt"); alpineCopyToContainer.copyFileToContainer(mountableFile, "/test.txt"); File actualFile = new File(temporaryFolder.toFile().getAbsolutePath() + "/test_copy_to_container.txt"); alpineCopyToContainer.copyFileFromContainer("/test.txt", actualFile.getPath()); File expectedFile = new File(mountableFile.getResolvedPath()); assertThat(FileUtils.contentEquals(expectedFile, actualFile)).as("Files aren't same ").isTrue(); } } @Test void copyLargeFilesToContainer() throws Exception { File tempFile = temporaryFolder.resolve("large-file").toFile(); try ( GenericContainer alpineCopyToContainer = new GenericContainer<>(TestImages.ALPINE_IMAGE) // .withCommand("sleep", "infinity") ) { alpineCopyToContainer.start(); final long byteCount; try ( FileOutputStream fos = new FileOutputStream(tempFile); CountingOutputStream cos = new CountingOutputStream(fos); BufferedOutputStream bos = new BufferedOutputStream(cos) ) { for (int i = 0; i < 0x4000; i++) { byte[] bytes = new byte[0xFFFF]; bos.write(bytes); } bos.flush(); byteCount = cos.getByteCount(); } final MountableFile mountableFile = MountableFile.forHostPath(tempFile.getPath()); final String containerPath = "/test.bin"; alpineCopyToContainer.copyFileToContainer(mountableFile, containerPath); final Container.ExecResult execResult = alpineCopyToContainer.execInContainer( // "stat", "-c", "%s", containerPath ); assertThat(execResult.getStdout()).isEqualToIgnoringNewLines(Long.toString(byteCount)); } finally { tempFile.delete(); } } @Test void copyFileToContainerFolderTest() throws Exception { try ( GenericContainer alpineCopyToContainer = new GenericContainer(TestImages.ALPINE_IMAGE) // .withCommand("top") ) { alpineCopyToContainer.start(); final MountableFile mountableFile = MountableFile.forClasspathResource("test_copy_to_container.txt"); alpineCopyToContainer.copyFileToContainer(mountableFile, "/home/"); File actualFile = new File(temporaryFolder.toFile().getAbsolutePath() + "/test_copy_to_container.txt"); alpineCopyToContainer.copyFileFromContainer("/home/test_copy_to_container.txt", actualFile.getPath()); File expectedFile = new File(mountableFile.getResolvedPath()); assertThat(FileUtils.contentEquals(expectedFile, actualFile)).as("Files aren't same ").isTrue(); } } @Test void copyFolderToContainerFolderTest() throws Exception { try ( GenericContainer alpineCopyToContainer = new GenericContainer(TestImages.ALPINE_IMAGE) // .withCommand("top") ) { alpineCopyToContainer.start(); final MountableFile mountableFile = MountableFile.forClasspathResource("mappable-resource/"); alpineCopyToContainer.copyFileToContainer(mountableFile, "/home/test/"); File actualFile = new File(temporaryFolder.toFile().getAbsolutePath() + "/test_copy_to_container.txt"); alpineCopyToContainer.copyFileFromContainer("/home/test/test-resource.txt", actualFile.getPath()); File expectedFile = new File(mountableFile.getResolvedPath() + "/test-resource.txt"); assertThat(FileUtils.contentEquals(expectedFile, actualFile)).as("Files aren't same ").isTrue(); } } @Test void copyFromContainerShouldFailBecauseNoFileTest() { assertThatThrownBy(() -> { try ( GenericContainer alpineCopyToContainer = new GenericContainer(TestImages.ALPINE_IMAGE) // .withCommand("top") ) { alpineCopyToContainer.start(); alpineCopyToContainer.copyFileFromContainer( "/home/test.txt", "src/test/resources/copy-from/test.txt" ); } }) .isInstanceOf(NotFoundException.class); } @Test void shouldCopyFileFromContainerTest() throws IOException { try ( GenericContainer alpineCopyToContainer = new GenericContainer(TestImages.ALPINE_IMAGE) // .withCommand("top") ) { alpineCopyToContainer.start(); final MountableFile mountableFile = MountableFile.forClasspathResource("test_copy_to_container.txt"); alpineCopyToContainer.copyFileToContainer(mountableFile, "/home/"); File actualFile = new File(temporaryFolder.toFile().getAbsolutePath() + "/test_copy_from_container.txt"); alpineCopyToContainer.copyFileFromContainer("/home/test_copy_to_container.txt", actualFile.getPath()); File expectedFile = new File(mountableFile.getResolvedPath()); assertThat(FileUtils.contentEquals(expectedFile, actualFile)).as("Files aren't same ").isTrue(); } } @Test void copyFileOperationsShouldFailWhenNotStartedTest() { try (GenericContainer container = new GenericContainer<>(TestImages.ALPINE_IMAGE).withCommand("top")) { assertThatThrownBy(() -> { MountableFile mountableFile = MountableFile.forClasspathResource("test_copy_to_container.txt"); container.copyFileToContainer(mountableFile, "/home/test.txt"); }) .isInstanceOf(IllegalStateException.class) .hasMessageContaining("can only be used with created / running container"); assertThatThrownBy(() -> { container.copyFileFromContainer("/home/test_copy_to_container.txt", IOUtils::toByteArray); }) .isInstanceOf(IllegalStateException.class) .hasMessageContaining("can only be used when the Container is created"); } } @Test void shouldCopyFileFromExitedContainerTest() { try ( GenericContainer container = new GenericContainer<>(TestImages.ALPINE_IMAGE) .withCommand("sh", "-c", "echo -n 'Hello!' > /home/file_in_container.txt") .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) ) { container.start(); assertThat( container.getDockerClient().waitContainerCmd(container.getContainerId()).start().awaitStatusCode() ) .isZero(); container.copyFileFromContainer("/home/file_in_container.txt", IOUtils::toByteArray); container.copyFileToContainer( MountableFile.forClasspathResource("test_copy_to_container.txt"), "/test.txt" ); } } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/FixedHostPortContainerTest.java ================================================ package org.testcontainers.junit; import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; import org.testcontainers.TestImages; import org.testcontainers.containers.FixedHostPortGenericContainer; import org.testcontainers.containers.GenericContainer; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.Socket; import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; /** * Test of {@link FixedHostPortGenericContainer}. Note that this is not an example of typical use (usually, a container * should be a field on the test class annotated with @Rule or @TestRule). Instead, here, the lifecycle of the container * is managed completely within the test method to allow a free port to be found and assigned before the container * is started. */ class FixedHostPortContainerTest { private static final String TEST_IMAGE = "alpine:3.17"; /** * Default http server port (just something different from default) */ private static final int TEST_PORT = 5678; /** * test response */ private static final String TEST_RESPONSE = "test-response"; /** * *nix pipe to fire test response on test port */ private static final String HTTP_ECHO_CMD = String.format( "while true; do echo \"%s\" | nc -l -p %d; done", TEST_RESPONSE, TEST_PORT ); @Test void testFixedHostPortMapping() throws IOException { // first find a free port on the docker host that will work for testing final Integer unusedHostPort; try ( final GenericContainer echoServer = new GenericContainer(TestImages.TINY_IMAGE) .withExposedPorts(TEST_PORT) .withCommand("/bin/sh", "-c", HTTP_ECHO_CMD) ) { echoServer.start(); unusedHostPort = echoServer.getMappedPort(TEST_PORT); } // now starting echo server container mapped to known-as-free host port try ( final GenericContainer echoServer = new FixedHostPortGenericContainer(TEST_IMAGE) // using workaround for port bind+expose .withFixedExposedPort(unusedHostPort, TEST_PORT) .withExposedPorts(TEST_PORT) .withCommand("/bin/sh", "-c", HTTP_ECHO_CMD) ) { echoServer.start(); assertThat(echoServer.getMappedPort(TEST_PORT)) .as("Port mapping does not seem to match given fixed port") .isEqualTo(unusedHostPort); final String content = readResponse(echoServer, unusedHostPort); assertThat(content).as("Returned echo from fixed port does not match expected").isEqualTo(TEST_RESPONSE); } } /** * Simple socket content reader from given container:port * * @param container to query * @param port to send request to * @return socket reader content * @throws IOException if any */ private String readResponse(GenericContainer container, Integer port) throws IOException { try ( Socket socket = Awaitility .await() .pollDelay(Duration.ofSeconds(1)) .until(() -> new Socket(container.getHost(), port), Socket::isConnected); BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())) ) { return reader.readLine(); } } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/GenericContainerRuleTest.java ================================================ package org.testcontainers.junit; import com.github.dockerjava.api.model.HostConfig; import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.Uninterruptibles; import com.mongodb.MongoClient; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; import com.rabbitmq.client.AMQP; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.DefaultConsumer; import com.rabbitmq.client.Envelope; import org.apache.commons.io.FileUtils; import org.bson.Document; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.rnorth.ducttape.RetryCountExceededException; import org.rnorth.ducttape.unreliables.Unreliables; import org.testcontainers.TestImages; import org.testcontainers.containers.BindMode; import org.testcontainers.containers.Container; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.SelinuxContext; import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; import org.testcontainers.utility.Base58; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; import java.net.Socket; import java.time.Duration; import java.util.Arrays; import java.util.Collections; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.regex.Matcher; import java.util.regex.Pattern; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; /** * Tests for GenericContainerRules */ class GenericContainerRuleTest { private static final int REDIS_PORT = 6379; private static final String RABBIQMQ_TEST_EXCHANGE = "TestExchange"; private static final String RABBITMQ_TEST_ROUTING_KEY = "TestRoutingKey"; private static final String RABBITMQ_TEST_MESSAGE = "Hello world"; private static final int RABBITMQ_PORT = 5672; private static final int MONGO_PORT = 27017; /* * Test data setup */ @BeforeAll public static void setupContent() throws FileNotFoundException { File contentFolder = new File(System.getProperty("user.home") + "/.tmp-test-container"); contentFolder.mkdir(); writeStringToFile(contentFolder, "file", "Hello world!"); } /** * Redis */ public static GenericContainer redis = new GenericContainer<>(TestImages.REDIS_IMAGE) .withExposedPorts(REDIS_PORT); /** * RabbitMQ */ public static GenericContainer rabbitMq = new GenericContainer<>(TestImages.RABBITMQ_IMAGE) .withExposedPorts(RABBITMQ_PORT); /** * MongoDB */ public static GenericContainer mongo = new GenericContainer<>(TestImages.MONGODB_IMAGE) .withExposedPorts(MONGO_PORT); /** * Pass an environment variable to the container, then run a shell script that exposes the variable in a quick and * dirty way for testing. */ public static GenericContainer alpineEnvVar = new GenericContainer<>(TestImages.ALPINE_IMAGE) .withExposedPorts(80) .withEnv("MAGIC_NUMBER", "4") .withEnv("MAGIC_NUMBER", oldValue -> oldValue.orElse("") + "2") .withCommand("/bin/sh", "-c", "while true; do echo \"$MAGIC_NUMBER\" | nc -l -p 80; done"); /** * Pass environment variables to the container, then run a shell script that exposes the variables in a quick and * dirty way for testing. */ public static GenericContainer alpineEnvVarFromMap = new GenericContainer<>(TestImages.ALPINE_IMAGE) .withExposedPorts(80) .withEnv(ImmutableMap.of("FIRST", "42", "SECOND", "50")) .withCommand("/bin/sh", "-c", "while true; do echo \"$FIRST and $SECOND\" | nc -l -p 80; done"); /** * Map a file on the classpath to a file in the container, and then expose the content for testing. */ public static GenericContainer alpineClasspathResource = new GenericContainer<>(TestImages.ALPINE_IMAGE) .withExposedPorts(80) .withClasspathResourceMapping("mappable-resource/test-resource.txt", "/content.txt", BindMode.READ_ONLY) .withCommand("/bin/sh", "-c", "while true; do cat /content.txt | nc -l -p 80; done"); /** * Map a file on the classpath to a file in the container, and then expose the content for testing. */ public static GenericContainer alpineClasspathResourceSelinux = new GenericContainer<>(TestImages.ALPINE_IMAGE) .withExposedPorts(80) .withClasspathResourceMapping( "mappable-resource/test-resource.txt", "/content.txt", BindMode.READ_WRITE, SelinuxContext.SHARED ) .withCommand("/bin/sh", "-c", "while true; do cat /content.txt | nc -l -p 80; done"); /** * Create a container with an extra host entry and expose the content of /etc/hosts for testing. */ public static GenericContainer alpineExtrahost = new GenericContainer<>(TestImages.ALPINE_IMAGE) .withExposedPorts(80) .withExtraHost("somehost", "192.168.1.10") .withCommand("/bin/sh", "-c", "while true; do cat /etc/hosts | nc -l -p 80; done"); static { redis.start(); rabbitMq.start(); mongo.start(); alpineEnvVar.start(); alpineEnvVarFromMap.start(); alpineClasspathResource.start(); alpineClasspathResourceSelinux.start(); alpineExtrahost.start(); } @Test void testIsRunning() { try (GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE).withCommand("top")) { assertThat(container.isRunning()).as("Container is not started and not running").isFalse(); container.start(); assertThat(container.isRunning()).as("Container is started and running").isTrue(); } } @Test void withTmpFsTest() throws Exception { try ( GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) .withCommand("top") .withTmpFs(Collections.singletonMap("/testtmpfs", "rw")) ) { container.start(); // check file doesn't exist String path = "/testtmpfs/test.file"; Container.ExecResult execResult = container.execInContainer("ls", path); assertThat(execResult.getStderr()) .as("tmpfs inside container works fine") .isEqualTo("ls: /testtmpfs/test.file: No such file or directory\n"); // touch && check file does exist container.execInContainer("touch", path); execResult = container.execInContainer("ls", path); assertThat(execResult.getStdout()).as("tmpfs inside container works fine").isEqualTo(path + "\n"); } } @Test void simpleRabbitMqTest() throws IOException, TimeoutException { ConnectionFactory factory = new ConnectionFactory(); factory.setHost(rabbitMq.getHost()); factory.setPort(rabbitMq.getMappedPort(RABBITMQ_PORT)); Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); channel.exchangeDeclare(RABBIQMQ_TEST_EXCHANGE, "direct", true); String queueName = channel.queueDeclare().getQueue(); channel.queueBind(queueName, RABBIQMQ_TEST_EXCHANGE, RABBITMQ_TEST_ROUTING_KEY); // Set up a consumer on the queue final boolean[] messageWasReceived = new boolean[1]; channel.basicConsume( queueName, false, new DefaultConsumer(channel) { @Override public void handleDelivery( String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body ) throws IOException { messageWasReceived[0] = Arrays.equals(body, RABBITMQ_TEST_MESSAGE.getBytes()); } } ); // post a message channel.basicPublish(RABBIQMQ_TEST_EXCHANGE, RABBITMQ_TEST_ROUTING_KEY, null, RABBITMQ_TEST_MESSAGE.getBytes()); // check the message was received assertThat( Unreliables.retryUntilSuccess( 5, TimeUnit.SECONDS, () -> { if (!messageWasReceived[0]) { throw new IllegalStateException("Message not received yet"); } return true; } ) ) .as("The message was received") .isTrue(); } @Test void simpleMongoDbTest() { MongoClient mongoClient = new MongoClient(mongo.getHost(), mongo.getMappedPort(MONGO_PORT)); MongoDatabase database = mongoClient.getDatabase("test"); MongoCollection collection = database.getCollection("testCollection"); Document doc = new Document("name", "foo").append("value", 1); collection.insertOne(doc); Document doc2 = collection.find(new Document("name", "foo")).first(); assertThat(doc2.get("value")).as("A record can be inserted into and retrieved from MongoDB").isEqualTo(1); } @Test void environmentAndCustomCommandTest() throws IOException { String line = getReaderForContainerPort80(alpineEnvVar).readLine(); assertThat(line).as("An environment variable can be passed into a command").isEqualTo("42"); } @Test void environmentFromMapTest() throws IOException { String line = getReaderForContainerPort80(alpineEnvVarFromMap).readLine(); assertThat(line).as("Environment variables can be passed into a command from a map").isEqualTo("42 and 50"); } @Test void customLabelTest() { try ( final GenericContainer alpineCustomLabel = new GenericContainer<>(TestImages.ALPINE_IMAGE) .withLabel("our.custom", "label") .withCommand("top") .withCreateContainerCmdModifier(cmd -> cmd.getLabels().put("scope", "local")) ) { alpineCustomLabel.start(); Map labels = alpineCustomLabel.getCurrentContainerInfo().getConfig().getLabels(); assertThat(labels).as("org.testcontainers label is present").containsKey("org.testcontainers"); assertThat(labels).as("org.testcontainers.lang label is present").containsKey("org.testcontainers.lang"); assertThat(labels) .as("org.testcontainers.lang label is present") .containsEntry("org.testcontainers.lang", "java"); assertThat(labels) .as("org.testcontainers.version label is present") .containsKey("org.testcontainers.version"); assertThat(labels).as("our.custom label is present").containsKey("our.custom"); assertThat(labels).as("our.custom label value is label").containsEntry("our.custom", "label"); assertThat(labels) .as("project label value is testcontainers-java") .containsEntry("project", "testcontainers-java"); assertThat(labels).as("scope label value is local").containsEntry("scope", "local"); } } @Test void exceptionThrownWhenTryingToOverrideTestcontainersLabels() { assertThat( catchThrowable(() -> { new GenericContainer<>(TestImages.ALPINE_IMAGE).withLabel("org.testcontainers.foo", "false"); }) ) .as("When trying to overwrite an 'org.testcontainers' label, withLabel() throws an exception") .isInstanceOf(IllegalArgumentException.class); } @Test void customClasspathResourceMappingTest() throws IOException { // Note: This functionality doesn't work if you are running your build inside a Docker container; // in that case this test will fail. String line = getReaderForContainerPort80(alpineClasspathResource).readLine(); assertThat(line) .as("Resource on the classpath can be mapped using calls to withClasspathResourceMapping") .isEqualTo("FOOBAR"); } @Test void customClasspathResourceMappingWithSelinuxTest() throws IOException { String line = getReaderForContainerPort80(alpineClasspathResourceSelinux).readLine(); assertThat(line) .as("Resource on the classpath can be mapped using calls to withClasspathResourceMappingSelinux") .isEqualTo("FOOBAR"); } @Test void exceptionThrownWhenMappedPortNotFound() { assertThat(catchThrowable(() -> redis.getMappedPort(666))) .as("When the requested port is not mapped, getMappedPort() throws an exception") .isInstanceOf(IllegalArgumentException.class); } protected static void writeStringToFile(File contentFolder, String filename, String string) throws FileNotFoundException { File file = new File(contentFolder, filename); PrintStream printStream = new PrintStream(new FileOutputStream(file)); printStream.println(string); printStream.close(); } @Test @Disabled //TODO investigate intermittent failures void failFastWhenContainerHaltsImmediately() { long startingTimeNano = System.nanoTime(); final GenericContainer failsImmediately = new GenericContainer<>(TestImages.ALPINE_IMAGE) .withCommand("/bin/sh", "-c", "return false") .withMinimumRunningDuration(Duration.ofMillis(100)); try { assertThat(catchThrowable(failsImmediately::start)) .as("When we start a container that halts immediately, an exception is thrown") .isInstanceOf(RetryCountExceededException.class); // Check how long it took, to verify that we ARE bailing out early. // Want to strike a balance here; too short and this test will fail intermittently // on slow systems and/or due to GC variation, too long, and we won't properly test // what we're intending to test. int allowedSecondsToFailure = GenericContainer.CONTAINER_RUNNING_TIMEOUT_SEC / 2; long completedTimeNano = System.nanoTime(); assertThat(completedTimeNano - startingTimeNano < TimeUnit.SECONDS.toNanos(allowedSecondsToFailure)) .as("container should not take long to start up") .isTrue(); } finally { failsImmediately.stop(); } } @Test void extraHostTest() throws IOException { BufferedReader br = getReaderForContainerPort80(alpineExtrahost); // read hosts file from container StringBuffer hosts = new StringBuffer(); String line = br.readLine(); while (line != null) { hosts.append(line); hosts.append("\n"); line = br.readLine(); } Matcher matcher = Pattern.compile("^192.168.1.10\\s.*somehost", Pattern.MULTILINE).matcher(hosts.toString()); assertThat(matcher.find()).as("The hosts file of container contains extra host").isTrue(); } @Test void createContainerCmdHookTest() { // Use random name to avoid the conflicts between the tests String randomName = Base58.randomString(5); try ( GenericContainer container = new GenericContainer<>(TestImages.REDIS_IMAGE) .withCommand("redis-server", "--help") .withCreateContainerCmdModifier(cmd -> cmd.withName("overrideMe")) // Preserves the order .withCreateContainerCmdModifier(cmd -> cmd.withName(randomName)) // Allows to override pre-configured values by GenericContainer .withCreateContainerCmdModifier(cmd -> cmd.withCmd("redis-server", "--port", "6379")) ) { container.start(); assertThat(container.getContainerInfo().getName()).as("Name is configured").isEqualTo("/" + randomName); assertThat(Arrays.toString(container.getContainerInfo().getConfig().getCmd())) .as("Command is configured") .isEqualTo("[redis-server, --port, 6379]"); } } private BufferedReader getReaderForContainerPort80(GenericContainer container) { return Unreliables.retryUntilSuccess( 10, TimeUnit.SECONDS, () -> { Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); Socket socket = new Socket(container.getHost(), container.getFirstMappedPort()); return new BufferedReader(new InputStreamReader(socket.getInputStream())); } ); } @Test void addExposedPortAfterWithExposedPortsTest() { redis.addExposedPort(8987); assertThat(redis.getExposedPorts()).as("Both ports should be exposed").hasSize(2); assertThat(redis.getExposedPorts()).as("withExposedPort should be exposed").contains(REDIS_PORT); assertThat(redis.getExposedPorts()).as("addExposedPort should be exposed").contains(8987); } @Test void addingExposedPortTwiceShouldNotFail() { redis.addExposedPort(8987); redis.addExposedPort(8987); assertThat(redis.getExposedPorts()).as("Both ports should be exposed").hasSize(2); // 2 ports = de-duplicated port 8897 and original port 6379 assertThat(redis.getExposedPorts()).as("withExposedPort should be exposed").contains(REDIS_PORT); assertThat(redis.getExposedPorts()).as("addExposedPort should be exposed").contains(8987); } @Test void sharedMemorySetTest() { try ( GenericContainer containerWithSharedMemory = new GenericContainer<>(TestImages.TINY_IMAGE) .withSharedMemorySize(42L * FileUtils.ONE_MB) .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) ) { containerWithSharedMemory.start(); HostConfig hostConfig = containerWithSharedMemory.getContainerInfo().getHostConfig(); assertThat(hostConfig.getShmSize()) .as("Shared memory not set on container") .isEqualTo(42L * FileUtils.ONE_MB); } } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/NonExistentImagePullTest.java ================================================ package org.testcontainers.junit; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.testcontainers.containers.ContainerFetchException; import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.DockerImageName; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; /** * Created by rnorth on 20/03/2016. */ class NonExistentImagePullTest { @Test @Timeout(60) void pullingNonExistentImageFailsGracefully() { assertThat( catchThrowable(() -> { new GenericContainer<>(DockerImageName.parse("testcontainers/nonexistent:latest")).getDockerImageName(); }) ) .as("Pulling a nonexistent container will cause an exception to be thrown") .isInstanceOf(ContainerFetchException.class); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/OutputStreamTest.java ================================================ package org.testcontainers.junit; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.TestImages; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.output.OutputFrame; import org.testcontainers.containers.output.Slf4jLogConsumer; import org.testcontainers.containers.output.ToStringConsumer; import org.testcontainers.containers.output.WaitingConsumer; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; /** * Simple test for following container output. */ class OutputStreamTest { private static final Logger LOGGER = LoggerFactory.getLogger(OutputStreamTest.class); public GenericContainer container = new GenericContainer(TestImages.ALPINE_IMAGE) .withCommand("ping -c 5 127.0.0.1"); @BeforeEach void setUp() { container.start(); } @AfterEach void tearDown() { container.stop(); } @Test @Timeout(value = 60) void testFetchStdout() throws TimeoutException { WaitingConsumer consumer = new WaitingConsumer(); container.followOutput(consumer, OutputFrame.OutputType.STDOUT); consumer.waitUntil( frame -> frame.getType() == OutputFrame.OutputType.STDOUT && frame.getUtf8String().contains("seq=2"), 30, TimeUnit.SECONDS ); } @Test @Timeout(value = 60) void testFetchStdoutWithTimeout() { WaitingConsumer consumer = new WaitingConsumer(); container.followOutput(consumer, OutputFrame.OutputType.STDOUT); assertThat( catchThrowable(() -> { consumer.waitUntil( frame -> { return ( frame.getType() == OutputFrame.OutputType.STDOUT && frame.getUtf8String().contains("seq=5") ); }, 2, TimeUnit.SECONDS ); }) ) .as("a TimeoutException should be thrown") .isInstanceOf(TimeoutException.class); } @Test @Timeout(value = 60) void testFetchStdoutWithNoLimit() throws TimeoutException { WaitingConsumer consumer = new WaitingConsumer(); container.followOutput(consumer, OutputFrame.OutputType.STDOUT); consumer.waitUntil(frame -> { return frame.getType() == OutputFrame.OutputType.STDOUT && frame.getUtf8String().contains("seq=2"); }); } @Test @Timeout(value = 60) void testLogConsumer() throws TimeoutException { WaitingConsumer waitingConsumer = new WaitingConsumer(); Slf4jLogConsumer logConsumer = new Slf4jLogConsumer(LOGGER); Consumer composedConsumer = logConsumer.andThen(waitingConsumer); container.followOutput(composedConsumer); waitingConsumer.waitUntil(frame -> { return frame.getType() == OutputFrame.OutputType.STDOUT && frame.getUtf8String().contains("seq=2"); }); } @Test @Timeout(value = 60) void testToStringConsumer() throws TimeoutException { WaitingConsumer waitingConsumer = new WaitingConsumer(); ToStringConsumer toStringConsumer = new ToStringConsumer(); Consumer composedConsumer = toStringConsumer.andThen(waitingConsumer); container.followOutput(composedConsumer); waitingConsumer.waitUntilEnd(30, TimeUnit.SECONDS); String utf8String = toStringConsumer.toUtf8String(); assertThat(utf8String).as("the expected first value was found").contains("seq=1"); assertThat(utf8String).as("the expected last value was found").contains("seq=4"); assertThat(utf8String).as("a non-expected value was found").doesNotContain("seq=42"); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/OutputStreamWithTTYTest.java ================================================ package org.testcontainers.junit; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AutoClose; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.testcontainers.TestImages; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.output.OutputFrame; import org.testcontainers.containers.output.Slf4jLogConsumer; import org.testcontainers.containers.output.ToStringConsumer; import org.testcontainers.containers.output.WaitingConsumer; import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; @Slf4j @Timeout(10) class OutputStreamWithTTYTest { @AutoClose public GenericContainer container = new GenericContainer<>(TestImages.ALPINE_IMAGE) .withCommand("ls -1") .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) .withCreateContainerCmdModifier(command -> command.withTty(true)); @BeforeEach void setUp() { container.start(); } @Test void testFetchStdout() throws TimeoutException { WaitingConsumer consumer = new WaitingConsumer(); container.followOutput(consumer, OutputFrame.OutputType.STDOUT); consumer.waitUntil( frame -> { return frame.getType() == OutputFrame.OutputType.STDOUT && frame.getUtf8String().contains("home"); }, 4, TimeUnit.SECONDS ); } @Test void testFetchStdoutWithTimeout() { WaitingConsumer consumer = new WaitingConsumer(); container.followOutput(consumer, OutputFrame.OutputType.STDOUT); assertThat( catchThrowable(() -> { consumer.waitUntil( frame -> { return ( frame.getType() == OutputFrame.OutputType.STDOUT && frame.getUtf8String().contains("qqq") ); }, 1, TimeUnit.SECONDS ); }) ) .as("a TimeoutException should be thrown") .isInstanceOf(TimeoutException.class); } @Test void testFetchStdoutWithNoLimit() throws TimeoutException { WaitingConsumer consumer = new WaitingConsumer(); container.followOutput(consumer, OutputFrame.OutputType.STDOUT); consumer.waitUntil(frame -> { return frame.getType() == OutputFrame.OutputType.STDOUT && frame.getUtf8String().contains("home"); }); } @Test void testLogConsumer() throws TimeoutException { WaitingConsumer waitingConsumer = new WaitingConsumer(); Slf4jLogConsumer logConsumer = new Slf4jLogConsumer(log); Consumer composedConsumer = logConsumer.andThen(waitingConsumer); container.followOutput(composedConsumer); waitingConsumer.waitUntil(frame -> { return frame.getType() == OutputFrame.OutputType.STDOUT && frame.getUtf8String().contains("home"); }); } @Test void testToStringConsumer() throws TimeoutException { WaitingConsumer waitingConsumer = new WaitingConsumer(); ToStringConsumer toStringConsumer = new ToStringConsumer(); Consumer composedConsumer = toStringConsumer.andThen(waitingConsumer); container.followOutput(composedConsumer); waitingConsumer.waitUntilEnd(4, TimeUnit.SECONDS); String utf8String = toStringConsumer.toUtf8String(); assertThat(utf8String).as("the expected first value was found").contains("home"); assertThat(utf8String).as("the expected last value was found").contains("media"); assertThat(utf8String).as("a non-expected value was found").doesNotContain("qqq"); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/ParameterizedDockerfileContainerTest.java ================================================ package org.testcontainers.junit; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedClass; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.testcontainers.containers.GenericContainer; import org.testcontainers.images.builder.ImageFromDockerfile; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; /** * Simple test case / demonstration of creating a fresh container image from a Dockerfile DSL when the test * is parameterized. */ @ParameterizedClass(name = "{0}") @MethodSource("data") class ParameterizedDockerfileContainerTest { private final String expectedVersion; public GenericContainer container; public ParameterizedDockerfileContainerTest(String baseImage, String expectedVersion) { container = new GenericContainer( new ImageFromDockerfile() .withDockerfileFromBuilder(builder -> { builder .from(baseImage) // Could potentially customise the image here, e.g. adding files, running // commands, etc. .build(); }) ) .withCommand("top"); container.start(); this.expectedVersion = expectedVersion; } public static Stream data() { return Stream.of( Arguments.of("alpine:3.12", "3.12"), Arguments.of("alpine:3.13", "3.13"), Arguments.of("alpine:3.14", "3.14"), Arguments.of("alpine:3.15", "3.15"), Arguments.of("alpine:3.16", "3.16") ); } @Test void simpleTest() throws Exception { final String release = container.execInContainer("cat", "/etc/alpine-release").getStdout(); assertThat(release).as("/etc/alpine-release starts with " + expectedVersion).startsWith(expectedVersion); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/WorkingDirectoryTest.java ================================================ package org.testcontainers.junit; import org.junit.jupiter.api.Test; import org.testcontainers.TestImages; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; import static org.assertj.core.api.Assertions.assertThat; /** * Created by rnorth on 26/07/2016. */ class WorkingDirectoryTest { public static GenericContainer container = new GenericContainer(TestImages.ALPINE_IMAGE) .withWorkingDirectory("/etc") .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) .withCommand("ls", "-al"); static { container.start(); } @Test void checkOutput() { String listing = container.getLogs(); assertThat(listing).as("Directory listing contains expected /etc content").contains("hostname"); assertThat(listing).as("Directory listing contains expected /etc content").contains("init.d"); assertThat(listing).as("Directory listing contains expected /etc content").contains("passwd"); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/wait/strategy/AbstractWaitStrategyTest.java ================================================ package org.testcontainers.junit.wait.strategy; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.rnorth.ducttape.RetryCountExceededException; import org.testcontainers.TestImages; import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.WaitStrategy; import java.time.Duration; import java.util.concurrent.atomic.AtomicBoolean; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; /** * Common test methods for {@link WaitStrategy} implementations. */ public abstract class AbstractWaitStrategyTest { static final long WAIT_TIMEOUT_MILLIS = 3000; /** * Indicates that the WaitStrategy has completed waiting successfully. */ AtomicBoolean ready; /** * Subclasses should return an instance of {@link W} that sets {@code ready} to {@code true}, * if the wait was successful. * * @param ready the AtomicBoolean on which to indicate success * @return WaitStrategy implementation */ @NotNull protected abstract W buildWaitStrategy(final AtomicBoolean ready); @BeforeEach public void setUp() { ready = new AtomicBoolean(false); } /** * Starts a GenericContainer with the Alpine image, passing the given {@code shellCommand} as * a parameter to {@literal sh -c} (the container CMD). * * @param shellCommand the shell command to execute * @return the (unstarted) container */ private GenericContainer startContainerWithCommand(String shellCommand) { return startContainerWithCommand(shellCommand, buildWaitStrategy(ready)); } /** * Starts a GenericContainer with the Alpine image, passing the given {@code shellCommand} as * a parameter to {@literal sh -c} (the container CMD) and apply a give wait strategy. * Note that the timeout will be overwritten if any with {@link #WAIT_TIMEOUT_MILLIS}. * @param shellCommand the shell command to execute * @param waitStrategy The wait strategy to apply * @return the (unstarted) container */ protected GenericContainer startContainerWithCommand(String shellCommand, WaitStrategy waitStrategy) { return startContainerWithCommand(shellCommand, waitStrategy, 8080); } protected GenericContainer startContainerWithCommand( String shellCommand, WaitStrategy waitStrategy, Integer... ports ) { // apply WaitStrategy to container return new GenericContainer<>(TestImages.ALPINE_IMAGE) .withExposedPorts(ports) .withCommand("sh", "-c", shellCommand) .waitingFor(waitStrategy.withStartupTimeout(Duration.ofMillis(WAIT_TIMEOUT_MILLIS))); } /** * Expects that the WaitStrategy returns successfully after connection to a container with a listening port. * * @param shellCommand the shell command to execute */ protected void waitUntilReadyAndSucceed(String shellCommand) { try (GenericContainer container = startContainerWithCommand(shellCommand)) { waitUntilReadyAndSucceed(container); } } /** * Expects that the WaitStrategy throws a {@link RetryCountExceededException} after unsuccessful connection * to a container with a listening port. * * @param shellCommand the shell command to execute */ protected void waitUntilReadyAndTimeout(String shellCommand) { try (GenericContainer container = startContainerWithCommand(shellCommand)) { waitUntilReadyAndTimeout(container); } } /** * Expects that the WaitStrategy throws a {@link RetryCountExceededException} after unsuccessful connection * to a container with a listening port. * * @param container the container to start */ protected void waitUntilReadyAndTimeout(GenericContainer container) { // start() blocks until successful or timeout assertThat(catchThrowable(container::start)) .as("an exception is thrown when timeout occurs (" + WAIT_TIMEOUT_MILLIS + "ms)") .isInstanceOf(ContainerLaunchException.class); } /** * Expects that the WaitStrategy returns successfully after connection to a container with a listening port. * * @param container the container to start */ protected void waitUntilReadyAndSucceed(GenericContainer container) { // start() blocks until successful or timeout container.start(); assertThat(ready) .as(String.format("Expected container to be ready after timeout of %sms", WAIT_TIMEOUT_MILLIS)) .isTrue(); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/wait/strategy/HostPortWaitStrategyTest.java ================================================ package org.testcontainers.junit.wait.strategy; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.testcontainers.TestImages; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import java.time.Duration; /** * Test wait strategy with overloaded waitingFor methods. * Other implementations of WaitStrategy are tested through backwards compatible wait strategy tests */ class HostPortWaitStrategyTest { @Nested class DefaultHostPortWaitStrategyTest { public GenericContainer container = new GenericContainer<>(TestImages.ALPINE_IMAGE) .withExposedPorts() .withCommand("sh", "-c", "while true; do nc -lp 8080; done") .withExposedPorts(8080) .waitingFor(Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(10))); @Test void testWaiting() { container.start(); } } @Nested class ExplicitHostPortWaitStrategyTest { public GenericContainer container = new GenericContainer<>(TestImages.ALPINE_IMAGE) .withExposedPorts() .withCommand("sh", "-c", "while true; do nc -lp 8080; done") .withExposedPorts(8080) .waitingFor(Wait.forListeningPorts(8080).withStartupTimeout(Duration.ofSeconds(10))); @Test void testWaiting() { container.start(); } } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/wait/strategy/HttpWaitStrategyTest.java ================================================ package org.testcontainers.junit.wait.strategy; import org.assertj.core.api.Assertions; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.rnorth.ducttape.RetryCountExceededException; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; import org.testcontainers.images.builder.ImageFromDockerfile; import java.time.Duration; import java.util.HashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Predicate; import static org.assertj.core.api.Assertions.assertThat; /** * Tests for {@link HttpWaitStrategy}. */ class HttpWaitStrategyTest extends AbstractWaitStrategyTest { /** * newline sequence indicating end of the HTTP header. */ private static final String NEWLINE = "\r\n"; private static final String GOOD_RESPONSE_BODY = "Good Response Body"; /** * Expects that the WaitStrategy returns successfully after receiving an HTTP 200 response from the container. */ @Test void testWaitUntilReadyWithSuccess() { waitUntilReadyAndSucceed(createShellCommand("200 OK", GOOD_RESPONSE_BODY)); } /** * Ensures that HTTP requests made with the HttpWaitStrategy can be enriched with user defined headers, * although the test web server does not depend on the header to response with a 200, by checking the * logs we can ensure the HTTP request was correctly sent. */ @Test void testWaitUntilReadyWithSuccessWithCustomHeaders() { HashMap headers = new HashMap<>(); headers.put("baz", "boo"); try ( GenericContainer container = startContainerWithCommand( createShellCommand("200 OK", GOOD_RESPONSE_BODY), createHttpWaitStrategy(ready).withHeader("foo", "bar").withHeaders(headers) ) ) { waitUntilReadyAndSucceed(container); String logs = container.getLogs(); assertThat(logs).contains("foo: bar"); assertThat(logs).contains("baz: boo"); } } /** * Ensures that HTTPS requests made with the HttpWaitStrategy can skip the * certificate validation chains (to support self-signed certificates for example). */ @Test void testWaitUntilReadyWithTlsAndAllowUnsecure() { try ( GenericContainer container = startContainerWithCommand( createHttpsShellCommand("200 OK", GOOD_RESPONSE_BODY, 8080), createHttpWaitStrategy(ready).usingTls().allowInsecure() ) ) { waitUntilReadyAndSucceed(container); } } /** * Expects that the WaitStrategy returns successfully after receiving an HTTP 401 response from the container. * This 401 response is checked with a lambda using {@link HttpWaitStrategy#forStatusCodeMatching(Predicate)} */ @Test void testWaitUntilReadyWithUnauthorizedWithLambda() { try ( GenericContainer container = startContainerWithCommand( createShellCommand("401 UNAUTHORIZED", GOOD_RESPONSE_BODY), createHttpWaitStrategy(ready).forStatusCodeMatching(it -> it >= 200 && it < 300 || it == 401) ) ) { waitUntilReadyAndSucceed(container); } } /** * Expects that the WaitStrategy returns successfully after receiving an HTTP 401 response from the container. * This 401 response is checked with many status codes using {@link HttpWaitStrategy#forStatusCode(int)} */ @Test void testWaitUntilReadyWithManyStatusCodes() { try ( GenericContainer container = startContainerWithCommand( createShellCommand("401 UNAUTHORIZED", GOOD_RESPONSE_BODY), createHttpWaitStrategy(ready).forStatusCode(300).forStatusCode(401).forStatusCode(500) ) ) { waitUntilReadyAndSucceed(container); } } /** * Expects that the WaitStrategy returns successfully after receiving an HTTP 401 response from the container. * This 401 response is checked with with many status codes using {@link HttpWaitStrategy#forStatusCode(int)} * and a lambda using {@link HttpWaitStrategy#forStatusCodeMatching(Predicate)} */ @Test void testWaitUntilReadyWithManyStatusCodesAndLambda() { try ( GenericContainer container = startContainerWithCommand( createShellCommand("401 UNAUTHORIZED", GOOD_RESPONSE_BODY), createHttpWaitStrategy(ready) .forStatusCode(300) .forStatusCode(500) .forStatusCodeMatching(it -> it == 401) ) ) { waitUntilReadyAndSucceed(container); } } /** * Expects that the WaitStrategy throws a {@link RetryCountExceededException} after not receiving any of the * error code defined with {@link HttpWaitStrategy#forStatusCode(int)} * and {@link HttpWaitStrategy#forStatusCodeMatching(Predicate)} */ @Test void testWaitUntilReadyWithTimeoutAndWithManyStatusCodesAndLambda() { try ( GenericContainer container = startContainerWithCommand( createShellCommand("401 UNAUTHORIZED", GOOD_RESPONSE_BODY), createHttpWaitStrategy(ready).forStatusCode(300).forStatusCodeMatching(it -> it == 500) ) ) { waitUntilReadyAndTimeout(container); } } /** * Expects that the WaitStrategy throws a {@link RetryCountExceededException} after not receiving any of the * error code defined with {@link HttpWaitStrategy#forStatusCode(int)} * and {@link HttpWaitStrategy#forStatusCodeMatching(Predicate)}. Note that a 200 status code should not * be considered as a successful return as not explicitly set. * Test case for: https://github.com/testcontainers/testcontainers-java/issues/880 */ @Test void testWaitUntilReadyWithTimeoutAndWithLambdaShouldNotMatchOk() { try ( GenericContainer container = startContainerWithCommand( createShellCommand("200 OK", GOOD_RESPONSE_BODY), createHttpWaitStrategy(ready).forStatusCodeMatching(it -> it >= 300) ) ) { waitUntilReadyAndTimeout(container); } } /** * Expects that the WaitStrategy throws a {@link RetryCountExceededException} after not receiving an HTTP 200 * response from the container within the timeout period. */ @Test void testWaitUntilReadyWithTimeout() { waitUntilReadyAndTimeout(createShellCommand("400 Bad Request", GOOD_RESPONSE_BODY)); } /** * Expects that the WaitStrategy throws a {@link RetryCountExceededException} after not the expected response body * from the container within the timeout period. */ @Test void testWaitUntilReadyWithTimeoutAndBadResponseBody() { waitUntilReadyAndTimeout(createShellCommand("200 OK", "Bad Response")); } /** * Expects the WaitStrategy probing the right port. */ @Test void testWaitUntilReadyWithSpecificPort() { try ( GenericContainer container = startContainerWithCommand( createShellCommand("200 OK", GOOD_RESPONSE_BODY, 9090), createHttpWaitStrategy(ready).forPort(9090), 7070, 8080, 9090 ) ) { waitUntilReadyAndSucceed(container); } } @Test void testWaitUntilReadyWithTimeoutCausedByReadTimeout() { try ( GenericContainer container = startContainerWithCommand( createShellCommand("0 Connection Refused", GOOD_RESPONSE_BODY, 9090), createHttpWaitStrategy(ready).forPort(9090).withReadTimeout(Duration.ofMillis(1)), 9090 ) ) { waitUntilReadyAndTimeout(container); } } /** * Test to validate fix from GitHub Pull Request #5778, i.e. when the container startup fails (ContainerLaunchException) before timeout for some reason, we are able to see the root cause of the error in the stack trace, e.g. in this case, a TLS certificate validation error during the TLS handshake test, because we are using a NGINX docker image with self-signed certificate created with the image, that is obviously not trusted. * The exceptions we should see in the stacktrace ('/' means 'caused by'): ContainerLaunchException / TimeoutException / RuntimeException / SSLHandshakeException / ValidatorException (in sun.* package so not accessible) / SunCertPathBuilderException (in sun.* package so not accessible). */ @Test void testWaitUntilReadyWithTimeoutCausedBySslHandshakeError() { try ( GenericContainer container = new GenericContainer<>( new ImageFromDockerfile() .withFileFromClasspath("Dockerfile", "https-wait-strategy-dockerfile/Dockerfile") .withFileFromClasspath("nginx-ssl.conf", "https-wait-strategy-dockerfile/nginx-ssl.conf") ) .withExposedPorts(8443) .waitingFor( createHttpWaitStrategy(ready) .forPort(8443) .usingTls() .withStartupTimeout(Duration.ofMillis(WAIT_TIMEOUT_MILLIS)) ) ) { Throwable throwable = Assertions.catchThrowable(container::start); assertThat(throwable).hasStackTraceContaining("javax.net.ssl.SSLHandshakeException"); } } /** * @param ready the AtomicBoolean on which to indicate success * @return the WaitStrategy under test */ @NotNull protected HttpWaitStrategy buildWaitStrategy(final AtomicBoolean ready) { return createHttpWaitStrategy(ready).forResponsePredicate(s -> s.equals(GOOD_RESPONSE_BODY)); } /** * Create a HttpWaitStrategy instance with a waitUntilReady implementation * * @param ready Indicates that the WaitStrategy has completed waiting successfully. * @return the HttpWaitStrategy instance */ private HttpWaitStrategy createHttpWaitStrategy(final AtomicBoolean ready) { return new HttpWaitStrategy() { @Override protected void waitUntilReady() { // blocks until ready or timeout occurs super.waitUntilReady(); ready.set(true); } }; } private String createShellCommand(String header, String responseBody) { return createShellCommand(header, responseBody, 8080); } private String createShellCommand(String header, String responseBody, int port) { int length = responseBody.getBytes().length; return ( "while true; do { echo -e \"HTTP/1.1 " + header + NEWLINE + "Content-Type: text/html" + NEWLINE + "Content-Length: " + length + NEWLINE + "\";" + " echo \"" + responseBody + "\";} | nc -lp " + port + "; done" ); } private String createHttpsShellCommand(String header, String responseBody, int port) { int length = responseBody.getBytes().length; return ( "apk add nmap-ncat; while true; do { echo -e \"HTTP/1.1 " + header + NEWLINE + "Content-Type: text/html" + NEWLINE + "Content-Length: " + length + NEWLINE + "\";" + " echo \"" + responseBody + "\";} | ncat -lp " + port + " --ssl; done" ); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/wait/strategy/LogMessageWaitStrategyTest.java ================================================ package org.testcontainers.junit.wait.strategy; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedClass; import org.junit.jupiter.params.provider.MethodSource; import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; import java.util.concurrent.atomic.AtomicBoolean; /** * Tests for {@link LogMessageWaitStrategy}. */ @ParameterizedClass(name = "{0}") @MethodSource("parameters") class LogMessageWaitStrategyTest extends AbstractWaitStrategyTest { private final String pattern; public static Object[] parameters() { return new String[] { ".*ready.*\\s", // previous recommended style (explicit line ending) ".*ready!\\s", // explicit line ending without wildcard after expected text ".*ready.*", // new style (line ending matched by wildcard) }; } public LogMessageWaitStrategyTest(String pattern) { this.pattern = pattern; } private static final String READY_MESSAGE = "I'm ready!"; @Test void testWaitUntilReady_Success() { waitUntilReadyAndSucceed( "echo -e \"" + READY_MESSAGE + "\";" + "echo -e \"foobar\";" + "echo -e \"" + READY_MESSAGE + "\";" + "sleep 300" ); } @Test void testWaitUntilReady_Timeout() { waitUntilReadyAndTimeout("echo -e \"" + READY_MESSAGE + "\";" + "echo -e \"foobar\";" + "sleep 300"); } @NotNull @Override protected LogMessageWaitStrategy buildWaitStrategy(AtomicBoolean ready) { return new LogMessageWaitStrategy() { @Override protected void waitUntilReady() { super.waitUntilReady(); ready.set(true); } } .withRegEx(pattern) .withTimes(2); } } ================================================ FILE: core/src/test/java/org/testcontainers/junit/wait/strategy/ShellStrategyTest.java ================================================ package org.testcontainers.junit.wait.strategy; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.testcontainers.containers.wait.strategy.ShellStrategy; import java.util.concurrent.atomic.AtomicBoolean; /** * Tests for {@link ShellStrategy}. */ class ShellStrategyTest extends AbstractWaitStrategyTest { private static final String LOCK_FILE = "/tmp/ready.lock"; @Test void testWaitUntilReady_Success() { waitUntilReadyAndSucceed(String.format("touch %s; sleep 300", LOCK_FILE)); } @Test void testWaitUntilReady_Timeout() { waitUntilReadyAndTimeout("sleep 300"); } @NotNull @Override protected ShellStrategy buildWaitStrategy(AtomicBoolean ready) { return createShellStrategy(ready).withCommand(String.format("stat %s", LOCK_FILE)); } @NotNull private static ShellStrategy createShellStrategy(AtomicBoolean ready) { return new ShellStrategy() { @Override protected void waitUntilReady() { super.waitUntilReady(); ready.set(true); } }; } } ================================================ FILE: core/src/test/java/org/testcontainers/utility/AuthenticatedImagePullTest.java ================================================ package org.testcontainers.utility; import com.github.dockerjava.api.model.AuthConfig; import org.intellij.lang.annotations.Language; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.testcontainers.DockerRegistryContainer; import org.testcontainers.TestImages; import org.testcontainers.containers.ContainerState; import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.containers.GenericContainer; import org.testcontainers.images.builder.ImageFromDockerfile; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.when; /** * This test checks the integration between Testcontainers and an authenticated registry, but uses * a mock instance of {@link RegistryAuthLocator} - the purpose of the test is solely to ensure that * the auth locator is utilised, and that the credentials it provides flow through to the registry. *

* {@link RegistryAuthLocatorTest} covers actual credential scenarios at a lower level, which are * impractical to test end-to-end. */ public class AuthenticatedImagePullTest { /** * Containerised docker image registry, with simple hardcoded credentials */ public static DockerRegistryContainer authenticatedRegistry = new DockerRegistryContainer( new ImageFromDockerfile() .withDockerfileFromBuilder(builder -> { builder .from(TestImages.DOCKER_REGISTRY_IMAGE.asCanonicalNameString()) .run("htpasswd -Bbn testuser notasecret > /htpasswd") .env("REGISTRY_AUTH", "htpasswd") .env("REGISTRY_AUTH_HTPASSWD_PATH", "/htpasswd") .env("REGISTRY_AUTH_HTPASSWD_REALM", "Test"); }) ); private static RegistryAuthLocator originalAuthLocatorSingleton; private final DockerImageName testImageName = authenticatedRegistry.createImage(); @BeforeAll public static void beforeClass() throws Exception { authenticatedRegistry.start(); originalAuthLocatorSingleton = RegistryAuthLocator.instance(); String testRegistryAddress = authenticatedRegistry.getEndpoint(); final AuthConfig authConfig = new AuthConfig() .withUsername("testuser") .withPassword("notasecret") .withRegistryAddress("http://" + testRegistryAddress); // Replace the RegistryAuthLocator singleton with our mock, for the duration of this test final RegistryAuthLocator mockAuthLocator = Mockito.mock(RegistryAuthLocator.class); RegistryAuthLocator.setInstance(mockAuthLocator); when( mockAuthLocator.lookupAuthConfig( argThat(argument -> testRegistryAddress.equals(argument.getRegistry())), any() ) ) .thenReturn(authConfig); } @AfterAll public static void tearDown() { RegistryAuthLocator.setInstance(originalAuthLocatorSingleton); } @Test void testThatAuthLocatorIsUsedForContainerCreation() { // actually start a container, which will require an authenticated pull try ( final GenericContainer container = new GenericContainer<>(testImageName) .withCommand("/bin/sh", "-c", "sleep 10") ) { container.start(); assertThat(container.isRunning()).as("container started following an authenticated pull").isTrue(); } } @Test void testThatAuthLocatorIsUsedForDockerfileBuild() throws IOException { // Prepare a simple temporary Dockerfile which requires our custom private image Path tempFile = getLocalTempFile(".Dockerfile"); String dockerFileContent = "FROM " + testImageName.asCanonicalNameString(); Files.write(tempFile, dockerFileContent.getBytes()); // Start a container built from a derived image, which will require an authenticated pull try ( final GenericContainer container = new GenericContainer<>( new ImageFromDockerfile().withDockerfile(tempFile) ) .withCommand("/bin/sh", "-c", "sleep 10") ) { container.start(); assertThat(container.isRunning()).as("container started following an authenticated pull").isTrue(); } } @Test void testThatAuthLocatorIsUsedForDockerComposePull() throws IOException { // Prepare a simple temporary Docker Compose manifest which requires our custom private image Path tempFile = getLocalTempFile(".docker-compose.yml"); @Language("yaml") String composeFileContent = "version: '2.0'\n" + "services:\n" + " privateservice:\n" + " command: /bin/sh -c 'sleep 60'\n" + " image: " + testImageName.asCanonicalNameString(); Files.write(tempFile, composeFileContent.getBytes()); // Start the docker compose project, which will require an authenticated pull try ( final DockerComposeContainer compose = new DockerComposeContainer<>( DockerImageName.parse("docker/compose:1.29.2"), tempFile.toFile() ) ) { compose.start(); assertThat( compose.getContainerByServiceName("privateservice_1").map(ContainerState::isRunning).orElse(false) ) .as("container started following an authenticated pull") .isTrue(); } } private Path getLocalTempFile(String s) throws IOException { Path projectRoot = Paths.get("."); Path tempDirectory = Files.createTempDirectory(projectRoot, this.getClass().getSimpleName() + "-test-"); Path relativeTempDirectory = projectRoot.relativize(tempDirectory); Path tempFile = Files.createTempFile(relativeTempDirectory, "test", s); tempDirectory.toFile().deleteOnExit(); tempFile.toFile().deleteOnExit(); return tempFile; } } ================================================ FILE: core/src/test/java/org/testcontainers/utility/ClasspathScannerTest.java ================================================ package org.testcontainers.utility; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import java.io.IOException; import java.net.URL; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class ClasspathScannerTest { private static URL FILE_A; private static URL FILE_B; private static URL JAR_A; private static URL JAR_B; private static URL FILE_C; @BeforeAll public static void setUp() throws Exception { FILE_A = new URL("file:///a/someName"); FILE_B = new URL("file:///b/someName"); FILE_C = new URL("file:///c/someName"); JAR_A = new URL("jar:file:a!/someName"); JAR_B = new URL("jar:file:b!/someName"); } @Test void realClassLoaderLookupOccurs() { // look for a resource that we know exists only once final List foundURLs = ClasspathScanner.scanFor("expectedClasspathFile.txt").collect(Collectors.toList()); assertThat(foundURLs).as("Exactly one resource was found").hasSize(1); } @Test void multipleResultsOnOneClassLoaderAreFound() throws IOException { final ClassLoader firstMockClassLoader = mock(ClassLoader.class); when(firstMockClassLoader.getResources(eq("someName"))) .thenReturn(Collections.enumeration(Arrays.asList(FILE_A, FILE_B))); final List foundURLs = ClasspathScanner .scanFor("someName", firstMockClassLoader) .collect(Collectors.toList()); assertThat(foundURLs).as("The expected URLs are found").containsExactly(FILE_A, FILE_B); } @Test void orderIsAlphabeticalForDeterminism() throws IOException { final ClassLoader firstMockClassLoader = mock(ClassLoader.class); when(firstMockClassLoader.getResources(eq("someName"))) .thenReturn(Collections.enumeration(Arrays.asList(FILE_B, JAR_A, JAR_B, FILE_A))); final List foundURLs = ClasspathScanner .scanFor("someName", firstMockClassLoader) .collect(Collectors.toList()); assertThat(foundURLs) .as("The expected URLs are found in the expected order") .containsExactly(FILE_A, FILE_B, JAR_A, JAR_B); } @Test void multipleClassLoadersAreQueried() throws IOException { final ClassLoader firstMockClassLoader = mock(ClassLoader.class); when(firstMockClassLoader.getResources(eq("someName"))) .thenReturn(Collections.enumeration(Arrays.asList(FILE_A, FILE_B))); final ClassLoader secondMockClassLoader = mock(ClassLoader.class); when(secondMockClassLoader.getResources(eq("someName"))) .thenReturn( Collections.enumeration( Arrays.asList( FILE_B, // duplicate FILE_C ) ) ); final List foundURLs = ClasspathScanner .scanFor("someName", firstMockClassLoader, secondMockClassLoader) .collect(Collectors.toList()); assertThat(foundURLs).as("The expected URLs are found").containsExactly(FILE_A, FILE_B, FILE_C); } } ================================================ FILE: core/src/test/java/org/testcontainers/utility/ComparableVersionTest.java ================================================ package org.testcontainers.utility; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import java.util.Arrays; import static org.assertj.core.api.Assertions.assertThat; class ComparableVersionTest { @ParameterizedTest(name = "Parsed version: {0}={1}") @MethodSource("data") void shouldParseVersions(String given, int[] expected) { assertThat(ComparableVersion.parseVersion(given)).containsExactly(expected); } public static Iterable data() { return Arrays.asList( new Object[][] { { "1.2.3", new int[] { 1, 2, 3 } }, { "", new int[0] }, { "1", new int[] { 1 } }, { "1.2.3.4.5.6.7", new int[] { 1, 2, 3, 4, 5, 6, 7 } }, { "1.2-dev", new int[] { 1, 2 } }, { "18.06.0-dev", new int[] { 18, 6 } }, } ); } } ================================================ FILE: core/src/test/java/org/testcontainers/utility/DefaultImageNameSubstitutorTest.java ================================================ package org.testcontainers.utility; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; @ExtendWith(MockTestcontainersConfigurationExtension.class) class DefaultImageNameSubstitutorTest { public static final DockerImageName ORIGINAL_IMAGE = DockerImageName.parse("foo"); public static final DockerImageName SUBSTITUTE_IMAGE = DockerImageName.parse("bar"); private ConfigurationFileImageNameSubstitutor underTest; @BeforeEach public void setUp() { underTest = new ConfigurationFileImageNameSubstitutor(TestcontainersConfiguration.getInstance()); } @Test void testConfigurationLookup() { Mockito .doReturn(SUBSTITUTE_IMAGE) .when(TestcontainersConfiguration.getInstance()) .getConfiguredSubstituteImage(eq(ORIGINAL_IMAGE)); final DockerImageName substitute = underTest.apply(ORIGINAL_IMAGE); assertThat(substitute).as("match is found").isEqualTo(SUBSTITUTE_IMAGE); assertThat(substitute.isCompatibleWith(ORIGINAL_IMAGE)).as("compatibility is automatically set").isTrue(); } } ================================================ FILE: core/src/test/java/org/testcontainers/utility/DirectoryTarResourceTest.java ================================================ package org.testcontainers.utility; import org.assertj.core.api.Condition; import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; import org.testcontainers.images.builder.ImageFromDockerfile; import java.io.File; import java.util.function.Predicate; import static org.assertj.core.api.Assertions.assertThat; class DirectoryTarResourceTest { @Test void simpleRecursiveFileTest() { // 'src' is expected to be the project base directory, so all source code/resources should be copied in File directory = new File("src"); GenericContainer container = new GenericContainer( new ImageFromDockerfile() .withDockerfileFromBuilder(builder -> { builder .from("alpine:3.17") .copy("/tmp/foo", "/foo") .cmd("cat /foo/test/resources/test-recursive-file.txt") .build(); }) .withFileFromFile("/tmp/foo", directory) ) .withStartupCheckStrategy(new OneShotStartupCheckStrategy()); container.start(); final String results = container.getLogs(); assertThat(results) .as("The container has a file that was copied in via a recursive copy") .contains("Used for DirectoryTarResourceTest"); } @Test void simpleRecursiveFileWithPermissionTest() { try ( GenericContainer container = new GenericContainer( new ImageFromDockerfile() .withDockerfileFromBuilder(builder -> { builder .from("alpine:3.17") // .copy("/tmp/foo", "/foo") .cmd("ls", "-al", "/") .build(); }) .withFileFromFile("/tmp/foo", new File("/mappable-resource/test-resource.txt"), 0754) ) .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) ) { container.start(); String listing = container.getLogs(); Predicate condition = s -> s.contains("-rwxr-xr--") && s.contains("foo"); assertThat(listing.split("\\n")) .as("Listing shows that file is copied with mode requested.") .haveAtLeastOne(new Condition<>(condition, "File not found in listing")); } } @Test void simpleRecursiveClasspathResourceTest() { // This test combines the copying of classpath resources from JAR files with the recursive TAR approach, to allow JARed classpath resources to be copied in to an image try ( GenericContainer container = new GenericContainer( new ImageFromDockerfile() .withDockerfileFromBuilder(builder -> { builder .from("alpine:3.17") // .copy("/tmp/foo", "/foo") .cmd("ls -lRt /foo") .build(); }) .withFileFromClasspath("/tmp/foo", "/recursive/dir") ) // here we use /org/junit as a directory that really should exist on the classpath .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) ) { container.start(); final String results = container.getLogs(); // ExternalResource.class is known to exist in a subdirectory of /org/junit so should be successfully copied in assertThat(results) .as("The container has a file that was copied in via a recursive copy from a JAR resource") .contains("content.txt"); } } } ================================================ FILE: core/src/test/java/org/testcontainers/utility/DockerImageNameCompatibilityTest.java ================================================ package org.testcontainers.utility; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class DockerImageNameCompatibilityTest { @Test void testPlainImage() { DockerImageName subject = DockerImageName.parse("foo"); assertThat(subject.isCompatibleWith(DockerImageName.parse("bar"))).as("image name foo != bar").isFalse(); } @Test void testNoTagTreatedAsWildcard() { final DockerImageName subject = DockerImageName.parse("foo:4.5.6"); /* foo:1.2.3 != foo:4.5.6 foo:1.2.3 ~= foo The test is effectively making sure that 'no tag' is treated as a wildcard */ assertThat(subject.isCompatibleWith(DockerImageName.parse("foo:1.2.3"))).as("foo:4.5.6 != foo:1.2.3").isFalse(); assertThat(subject.isCompatibleWith(DockerImageName.parse("foo"))).as("foo:4.5.6 ~= foo").isTrue(); } @Test void testImageWithAutomaticCompatibilityForFullPath() { DockerImageName subject = DockerImageName.parse("repo/foo:1.2.3"); assertThat(subject.isCompatibleWith(DockerImageName.parse("repo/foo"))) .as("repo/foo:1.2.3 ~= repo/foo") .isTrue(); } @Test void testImageWithClaimedCompatibility() { DockerImageName subject = DockerImageName.parse("foo").asCompatibleSubstituteFor("bar"); assertThat(subject.isCompatibleWith(DockerImageName.parse("bar"))).as("foo(bar) ~= bar").isTrue(); assertThat(subject.isCompatibleWith(DockerImageName.parse("fizz"))).as("foo(bar) != fizz").isFalse(); } @Test void testImageWithClaimedCompatibilityAndVersion() { DockerImageName subject = DockerImageName.parse("foo:1.2.3").asCompatibleSubstituteFor("bar"); assertThat(subject.isCompatibleWith(DockerImageName.parse("bar"))).as("foo:1.2.3(bar) ~= bar").isTrue(); } @Test void testImageWithClaimedCompatibilityForFullPath() { DockerImageName subject = DockerImageName.parse("foo").asCompatibleSubstituteFor("registry/repo/bar"); assertThat(subject.isCompatibleWith(DockerImageName.parse("registry/repo/bar"))) .as("foo(registry/repo/bar) ~= registry/repo/bar") .isTrue(); assertThat(subject.isCompatibleWith(DockerImageName.parse("repo/bar"))) .as("foo(registry/repo/bar) != repo/bar") .isFalse(); assertThat(subject.isCompatibleWith(DockerImageName.parse("bar"))) .as("foo(registry/repo/bar) != bar") .isFalse(); } @Test void testImageWithClaimedCompatibilityForVersion() { DockerImageName subject = DockerImageName.parse("foo").asCompatibleSubstituteFor("bar:1.2.3"); assertThat(subject.isCompatibleWith(DockerImageName.parse("bar"))).as("foo(bar:1.2.3) ~= bar").isTrue(); assertThat(subject.isCompatibleWith(DockerImageName.parse("bar:1.2.3"))) .as("foo(bar:1.2.3) ~= bar:1.2.3") .isTrue(); assertThat(subject.isCompatibleWith(DockerImageName.parse("bar:0.0.1"))) .as("foo(bar:1.2.3) != bar:0.0.1") .isFalse(); assertThat(subject.isCompatibleWith(DockerImageName.parse("bar:2.0.0"))) .as("foo(bar:1.2.3) != bar:2.0.0") .isFalse(); assertThat(subject.isCompatibleWith(DockerImageName.parse("bar:1.2.4"))) .as("foo(bar:1.2.3) != bar:1.2.4") .isFalse(); } @Test void testAssertMethodAcceptsCompatible() { DockerImageName subject = DockerImageName.parse("foo").asCompatibleSubstituteFor("bar"); subject.assertCompatibleWith(DockerImageName.parse("bar")); } @Test void testAssertMethodAcceptsCompatibleLibraryPrefix() { DockerImageName subject = DockerImageName.parse("library/foo"); subject.assertCompatibleWith(DockerImageName.parse("foo")); } @Test void testAssertMethodRejectsIncompatible() { DockerImageName subject = DockerImageName.parse("foo"); assertThatThrownBy(() -> subject.assertCompatibleWith(DockerImageName.parse("bar"))) .isInstanceOf(IllegalStateException.class) .hasMessageContaining("Failed to verify that image 'foo' is a compatible substitute for 'bar'"); } } ================================================ FILE: core/src/test/java/org/testcontainers/utility/DockerImageNameTest.java ================================================ package org.testcontainers.utility; import org.junit.jupiter.api.Nested; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class DockerImageNameTest { @Nested class ValidNames { public static String[] getNames() { return new String[] { "myname:latest", "repo/my-name:1.0", "registry.foo.com:1234/my-name:1.0", "registry.foo.com/my-name:1.0", "registry.foo.com:1234/repo_here/my-name:1.0", "registry.foo.com:1234/repo-here/my-name@sha256:1234abcd1234abcd1234abcd1234abcd", "registry.foo.com:1234/my-name@sha256:1234abcd1234abcd1234abcd1234abcd", "1.2.3.4/my-name:1.0", "1.2.3.4:1234/my-name:1.0", "1.2.3.4/repo-here/my-name:1.0", "1.2.3.4:1234/repo-here/my-name:1.0", }; } @ParameterizedTest @MethodSource("getNames") void testValidNameAccepted(String imageName) { DockerImageName.parse(imageName).assertValid(); } } @Nested class InvalidNames { public static String[] getNames() { return new String[] { ":latest", "/myname:latest", "/myname@sha256:latest", "/myname@sha256:gggggggggggggggggggggggggggggggg", "repo:notaport/myname:latest", }; } @ParameterizedTest @MethodSource("getNames") void testInvalidNameRejected(String imageName) { assertThatThrownBy(() -> DockerImageName.parse(imageName).assertValid()) .isInstanceOf(IllegalArgumentException.class); } } @Nested class Parsing { public static Stream getNames() { return Stream.of( Arguments.of("", "", "myname", ":", null), Arguments.of("", "", "myname", ":", "latest"), Arguments.of("", "", "repo/myname", ":", null), Arguments.of("", "", "repo/myname", ":", "latest"), Arguments.of("registry.foo.com:1234", "/", "my-name", ":", null), Arguments.of("registry.foo.com:1234", "/", "my-name", ":", "1.0"), Arguments.of("registry.foo.com", "/", "my-name", ":", "1.0"), Arguments.of("registry.foo.com:1234", "/", "repo_here/my-name", ":", null), Arguments.of("registry.foo.com:1234", "/", "repo_here/my-name", ":", "1.0"), Arguments.of("1.2.3.4:1234", "/", "repo_here/my-name", ":", null), Arguments.of("1.2.3.4:1234", "/", "repo_here/my-name", ":", "1.0"), Arguments.of("1.2.3.4:1234", "/", "my-name", ":", null), Arguments.of("1.2.3.4:1234", "/", "my-name", ":", "1.0"), Arguments.of("", "", "myname", "@", "sha256:1234abcd1234abcd1234abcd1234abcd"), Arguments.of("", "", "repo/myname", "@", "sha256:1234abcd1234abcd1234abcd1234abcd"), Arguments.of( "registry.foo.com:1234", "/", "repo-here/my-name", "@", "sha256:1234abcd1234abcd1234abcd1234abcd" ), Arguments.of("registry.foo.com:1234", "/", "my-name", "@", "sha256:1234abcd1234abcd1234abcd1234abcd"), Arguments.of("1.2.3.4", "/", "my-name", "@", "sha256:1234abcd1234abcd1234abcd1234abcd"), Arguments.of("1.2.3.4:1234", "/", "my-name", "@", "sha256:1234abcd1234abcd1234abcd1234abcd"), Arguments.of("1.2.3.4", "/", "my-name", "@", "sha256:1234abcd1234abcd1234abcd1234abcd"), Arguments.of("1.2.3.4:1234", "/", "my-name", "@", "sha256:1234abcd1234abcd1234abcd1234abcd") ); } @ParameterizedTest @MethodSource("getNames") void testParsing( String registry, String registrySeparator, String repo, String versionSeparator, String version ) { final String unversionedPart = registry + registrySeparator + repo; String combined; String canonicalName; if (version != null) { combined = unversionedPart + versionSeparator + version; canonicalName = unversionedPart + versionSeparator + version; } else { combined = unversionedPart; canonicalName = unversionedPart + ":latest"; } final DockerImageName imageName = DockerImageName.parse(combined); assertThat(imageName.getRegistry()).as(combined + " has registry address: " + registry).isEqualTo(registry); assertThat(imageName.getUnversionedPart()) .as(combined + " has unversioned part: " + unversionedPart) .isEqualTo(unversionedPart); if (version != null) { assertThat(imageName.getVersionPart()) .as(combined + " has version part: " + version) .isEqualTo(version); } else { assertThat(imageName.getVersionPart()) .as(combined + " has automatic 'latest' version specified") .isEqualTo("latest"); } assertThat(imageName.asCanonicalNameString()) .as(combined + " has canonical name: " + canonicalName) .isEqualTo(canonicalName); if (version != null) { final DockerImageName imageNameFromSecondaryConstructor = new DockerImageName(unversionedPart, version); assertThat(imageNameFromSecondaryConstructor.getRegistry()) .as(combined + " has registry address: " + registry) .isEqualTo(registry); assertThat(imageNameFromSecondaryConstructor.getUnversionedPart()) .as(combined + " has unversioned part: " + unversionedPart) .isEqualTo(unversionedPart); assertThat(imageNameFromSecondaryConstructor.getVersionPart()) .as(combined + " has version part: " + version) .isEqualTo(version); } } } } ================================================ FILE: core/src/test/java/org/testcontainers/utility/DockerLoggerFactoryTest.java ================================================ package org.testcontainers.utility; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.read.ListAppender; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class DockerLoggerFactoryTest { private static final Logger LOGGER = (Logger) DockerLoggerFactory.getLogger("dockerImageName"); @Test void debugIsNotSwallowedForContainerLogs() { ListAppender listAppender = new ListAppender<>(); listAppender.start(); LOGGER.addAppender(listAppender); LOGGER.debug("some text"); assertThat(listAppender.list).withFailMessage("Log message has been swallowed").hasSize(1); ILoggingEvent event = listAppender.list.get(0); assertThat(event.getFormattedMessage()).isEqualTo("some text"); assertThat(event.getLevel()).isEqualTo(Level.DEBUG); assertThat(event.getLoggerName()).startsWith("tc"); } } ================================================ FILE: core/src/test/java/org/testcontainers/utility/DockerStatusTest.java ================================================ package org.testcontainers.utility; import com.github.dockerjava.api.command.InspectContainerResponse; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedClass; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mockito; import java.time.Duration; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; @ParameterizedClass @MethodSource("parameters") class DockerStatusTest { private DateTimeFormatter dateTimeFormatter; private static final Instant now = Instant.now(); private final InspectContainerResponse.ContainerState running; private final InspectContainerResponse.ContainerState runningVariant; private final InspectContainerResponse.ContainerState shortRunning; private final InspectContainerResponse.ContainerState created; // a container in the "created" state is not running, and has both startedAt and finishedAt empty. private final InspectContainerResponse.ContainerState createdVariant; private final InspectContainerResponse.ContainerState exited; private final InspectContainerResponse.ContainerState paused; private static final Duration minimumDuration = Duration.ofMillis(20); public DockerStatusTest(DateTimeFormatter dateTimeFormatter) { this.dateTimeFormatter = dateTimeFormatter; running = buildState(true, false, buildTimestamp(now.minusMillis(30)), DockerStatus.DOCKER_TIMESTAMP_ZERO); runningVariant = buildState(true, false, buildTimestamp(now.minusMillis(30)), ""); shortRunning = buildState(true, false, buildTimestamp(now.minusMillis(10)), DockerStatus.DOCKER_TIMESTAMP_ZERO); created = buildState(false, false, DockerStatus.DOCKER_TIMESTAMP_ZERO, DockerStatus.DOCKER_TIMESTAMP_ZERO); createdVariant = buildState(false, false, null, null); exited = buildState(false, false, buildTimestamp(now.minusMillis(100)), buildTimestamp(now.minusMillis(90))); paused = buildState(false, true, buildTimestamp(now.minusMillis(100)), DockerStatus.DOCKER_TIMESTAMP_ZERO); } public static Stream parameters() { return Stream.of( DateTimeFormatter.ISO_INSTANT, DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneId.of("America/New_York")) ); } @Test void testRunning() { assertThat(DockerStatus.isContainerRunning(running, minimumDuration, now)).isTrue(); assertThat(DockerStatus.isContainerRunning(runningVariant, minimumDuration, now)).isTrue(); assertThat(DockerStatus.isContainerRunning(shortRunning, minimumDuration, now)).isFalse(); assertThat(DockerStatus.isContainerRunning(created, minimumDuration, now)).isFalse(); assertThat(DockerStatus.isContainerRunning(createdVariant, minimumDuration, now)).isFalse(); assertThat(DockerStatus.isContainerRunning(exited, minimumDuration, now)).isFalse(); assertThat(DockerStatus.isContainerRunning(paused, minimumDuration, now)).isFalse(); } @Test void testStopped() { assertThat(DockerStatus.isContainerStopped(running)).isFalse(); assertThat(DockerStatus.isContainerStopped(runningVariant)).isFalse(); assertThat(DockerStatus.isContainerStopped(shortRunning)).isFalse(); assertThat(DockerStatus.isContainerStopped(created)).isFalse(); assertThat(DockerStatus.isContainerStopped(createdVariant)).isFalse(); assertThat(DockerStatus.isContainerStopped(exited)).isTrue(); assertThat(DockerStatus.isContainerStopped(paused)).isFalse(); } private String buildTimestamp(Instant instant) { return dateTimeFormatter.format(instant); } // ContainerState is a non-static inner class, with private member variables, in a different package. // It's simpler to mock it that to try to create one. private static InspectContainerResponse.ContainerState buildState( boolean running, boolean paused, String startedAt, String finishedAt ) { InspectContainerResponse.ContainerState state = Mockito.mock(InspectContainerResponse.ContainerState.class); when(state.getRunning()).thenReturn(running); when(state.getPaused()).thenReturn(paused); when(state.getStartedAt()).thenReturn(startedAt); when(state.getFinishedAt()).thenReturn(finishedAt); return state; } } ================================================ FILE: core/src/test/java/org/testcontainers/utility/FakeImagePullPolicy.java ================================================ package org.testcontainers.utility; import org.testcontainers.images.AbstractImagePullPolicy; import org.testcontainers.images.ImageData; public class FakeImagePullPolicy extends AbstractImagePullPolicy { @Override protected boolean shouldPullCached(DockerImageName imageName, ImageData localImageData) { return false; } } ================================================ FILE: core/src/test/java/org/testcontainers/utility/FakeImageSubstitutor.java ================================================ package org.testcontainers.utility; public class FakeImageSubstitutor extends ImageNameSubstitutor { @Override public DockerImageName apply(final DockerImageName original) { return DockerImageName.parse("transformed-" + original.asCanonicalNameString()); } @Override protected String getDescription() { return "test implementation"; } } ================================================ FILE: core/src/test/java/org/testcontainers/utility/FilterRegistryTest.java ================================================ package org.testcontainers.utility; import org.junit.jupiter.api.Test; import org.testcontainers.utility.ResourceReaper.FilterRegistry; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.AbstractMap.SimpleEntry; import java.util.Arrays; import java.util.List; import java.util.Map.Entry; import static org.assertj.core.api.Assertions.assertThat; class FilterRegistryTest { private static final List> FILTERS = Arrays.asList( new SimpleEntry<>("key1!", "value2?"), new SimpleEntry<>("key2#", "value2%") ); private static final String URL_ENCODED_FILTERS = "key1%21=value2%3F&key2%23=value2%25"; private static final byte[] ACKNOWLEDGEMENT = FilterRegistry.ACKNOWLEDGMENT.getBytes(); private static final byte[] NO_ACKNOWLEDGEMENT = "".getBytes(); private static final String NEW_LINE = "\n"; @Test void registerReturnsTrueIfAcknowledgementIsReadFromInputStream() throws IOException { FilterRegistry registry = new FilterRegistry(inputStream(ACKNOWLEDGEMENT), anyOutputStream()); boolean successful = registry.register(FILTERS); assertThat(successful).isTrue(); } @Test void registerReturnsFalseIfNoAcknowledgementIsReadFromInputStream() throws IOException { FilterRegistry registry = new FilterRegistry(inputStream(NO_ACKNOWLEDGEMENT), anyOutputStream()); boolean successful = registry.register(FILTERS); assertThat(successful).isFalse(); } @Test void registerWritesUrlEncodedFiltersAndNewlineToOutputStream() throws IOException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); FilterRegistry registry = new FilterRegistry(anyInputStream(), outputStream); registry.register(FILTERS); assertThat(new String(outputStream.toByteArray())).isEqualTo(URL_ENCODED_FILTERS + NEW_LINE); } private static InputStream inputStream(byte[] bytes) { return new ByteArrayInputStream(bytes); } private static InputStream anyInputStream() { return inputStream(ACKNOWLEDGEMENT); } private static OutputStream anyOutputStream() { return new ByteArrayOutputStream(); } } ================================================ FILE: core/src/test/java/org/testcontainers/utility/ImageNameSubstitutorTest.java ================================================ package org.testcontainers.utility; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; import org.mockito.Mockito; import org.testcontainers.containers.GenericContainer; import java.io.FileWriter; import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.eq; @ExtendWith(MockTestcontainersConfigurationExtension.class) class ImageNameSubstitutorTest { @TempDir public Path tempFolder; private ImageNameSubstitutor originalInstance; private ImageNameSubstitutor originalDefaultImplementation; @BeforeEach public void setUp() throws Exception { originalInstance = ImageNameSubstitutor.instance; originalDefaultImplementation = ImageNameSubstitutor.defaultImplementation; ImageNameSubstitutor.instance = null; ImageNameSubstitutor.defaultImplementation = Mockito.mock(ImageNameSubstitutor.class); Mockito .doReturn(DockerImageName.parse("substituted-image")) .when(ImageNameSubstitutor.defaultImplementation) .apply(eq(DockerImageName.parse("original"))); Mockito.doReturn("default implementation").when(ImageNameSubstitutor.defaultImplementation).getDescription(); } @AfterEach public void tearDown() throws Exception { ImageNameSubstitutor.instance = originalInstance; ImageNameSubstitutor.defaultImplementation = originalDefaultImplementation; } @Test void simpleConfigurationTest() { Mockito .doReturn(FakeImageSubstitutor.class.getCanonicalName()) .when(TestcontainersConfiguration.getInstance()) .getImageSubstitutorClassName(); final ImageNameSubstitutor imageNameSubstitutor = ImageNameSubstitutor.instance(); DockerImageName result = imageNameSubstitutor.apply(DockerImageName.parse("original")); assertThat(result.asCanonicalNameString()) .as("the image has been substituted by default then configured implementations") .isEqualTo("transformed-substituted-image:latest"); } @Test void testWorksWithoutConfiguredImplementation() { Mockito.doReturn(null).when(TestcontainersConfiguration.getInstance()).getImageSubstitutorClassName(); final ImageNameSubstitutor imageNameSubstitutor = ImageNameSubstitutor.instance(); DockerImageName result = imageNameSubstitutor.apply(DockerImageName.parse("original")); assertThat(result.asCanonicalNameString()) .as("the image has been substituted by default then configured implementations") .isEqualTo("substituted-image:latest"); } @Test void testImageNameSubstitutorToString() { Mockito .doReturn(FakeImageSubstitutor.class.getCanonicalName()) .when(TestcontainersConfiguration.getInstance()) .getImageSubstitutorClassName(); try (GenericContainer container = new GenericContainer<>(DockerImageName.parse("original"))) { assertThatThrownBy(container::start) .hasMessageContaining( "imageNameSubstitutor=Chained substitutor of 'default implementation' and then 'test implementation'" ); } } @Test void testImageNameSubstitutorFromServiceLoader() throws IOException { Path tempDir = this.tempFolder.resolve("image-name-substitutor-test"); Path metaInfDir = Paths.get(tempDir.toString(), "META-INF", "services"); Files.createDirectories(metaInfDir); createClassFile(tempDir, "org/testcontainers/utility/ImageNameSubstitutor.class", ImageNameSubstitutor.class); createClassFile(tempDir, "org/testcontainers/utility/FakeImageSubstitutor.class", FakeImageSubstitutor.class); // Create service provider configuration file createServiceProviderFile( metaInfDir, "org.testcontainers.utility.ImageNameSubstitutor", "org.testcontainers.utility.FakeImageSubstitutor" ); URL[] urls = { tempDir.toUri().toURL() }; URLClassLoader classLoader = new URLClassLoader(urls, ImageNameSubstitutorTest.class.getClassLoader()); final ImageNameSubstitutor imageNameSubstitutor = ImageNameSubstitutor.instance(classLoader); DockerImageName result = imageNameSubstitutor.apply(DockerImageName.parse("original")); assertThat(result.asCanonicalNameString()) .as("the image has been substituted by default then configured implementations") .isEqualTo("transformed-substituted-image:latest"); } private void createClassFile(Path tempDir, String classFilePath, Class clazz) throws IOException { Path classFile = Paths.get(tempDir.toString(), classFilePath); Files.createDirectories(classFile.getParent()); Files.copy(clazz.getResourceAsStream("/" + classFilePath), classFile); } private void createServiceProviderFile(Path metaInfDir, String serviceInterface, String... implementations) throws IOException { Path serviceFile = Paths.get(metaInfDir.toString(), serviceInterface); try (FileWriter writer = new FileWriter(serviceFile.toFile())) { for (String impl : implementations) { writer.write(impl + "\n"); } } } } ================================================ FILE: core/src/test/java/org/testcontainers/utility/LazyFutureTest.java ================================================ package org.testcontainers.utility; import com.google.common.util.concurrent.Futures; import lombok.SneakyThrows; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import java.util.stream.IntStream; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; class LazyFutureTest { @Test void testLaziness() throws Exception { AtomicInteger counter = new AtomicInteger(); Future lazyFuture = new LazyFuture() { @Override protected Integer resolve() { return counter.incrementAndGet(); } }; assertThat(counter).as("No resolve() invocations before get()").hasValue(0); assertThat(lazyFuture.get()).as("get() call returns proper result").isEqualTo(1); assertThat(counter).as("resolve() was called only once after single get() call").hasValue(1); counter.incrementAndGet(); assertThat(lazyFuture.get()).as("result of resolve() must be cached").isEqualTo(1); } @Test @Timeout(5) void timeoutWorks() { Future lazyFuture = new LazyFuture() { @Override @SneakyThrows(InterruptedException.class) protected Void resolve() { TimeUnit.MINUTES.sleep(1); return null; } }; assertThat(catchThrowable(() -> lazyFuture.get(10, TimeUnit.MILLISECONDS))) .as("Should timeout") .isInstanceOf(TimeoutException.class); } @Test @Timeout(5) void testThreadSafety() throws Exception { final int numOfThreads = 3; CountDownLatch latch = new CountDownLatch(numOfThreads); AtomicInteger counter = new AtomicInteger(); Future lazyFuture = new LazyFuture() { @Override @SneakyThrows(InterruptedException.class) protected Integer resolve() { latch.await(); return counter.incrementAndGet(); } }; Future> task = new ForkJoinPool(numOfThreads) .submit(() -> { return IntStream .rangeClosed(1, numOfThreads) .parallel() .mapToObj(i -> Futures.getUnchecked(lazyFuture)) .collect(Collectors.toList()); }); while (latch.getCount() > 0) { latch.countDown(); } assertThat(task.get()) .as("All threads receives the same result") .isEqualTo(Collections.nCopies(numOfThreads, 1)); } } ================================================ FILE: core/src/test/java/org/testcontainers/utility/LicenseAcceptanceTest.java ================================================ package org.testcontainers.utility; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThatThrownBy; class LicenseAcceptanceTest { @Test void testForExistingNames() { LicenseAcceptance.assertLicenseAccepted("a"); LicenseAcceptance.assertLicenseAccepted("b"); } @Test void testForMissingNames() { assertThatThrownBy(() -> LicenseAcceptance.assertLicenseAccepted("c")) .isInstanceOf(IllegalStateException.class); } } ================================================ FILE: core/src/test/java/org/testcontainers/utility/MockTestcontainersConfigurationExtension.java ================================================ package org.testcontainers.utility; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.mockito.Mockito; import java.util.concurrent.atomic.AtomicReference; /** * This {@link org.junit.jupiter.api.extension.Extension} applies a spy on {@link TestcontainersConfiguration} * for testing features that depend on the global configuration. */ public class MockTestcontainersConfigurationExtension implements BeforeEachCallback, AfterEachCallback { private static final ExtensionContext.Namespace NS = ExtensionContext.Namespace.create( MockTestcontainersConfigurationExtension.class ); static AtomicReference REF = TestcontainersConfiguration.getInstanceField(); @Override public void beforeEach(ExtensionContext context) throws Exception { TestcontainersConfiguration previous = REF.get(); if (previous == null) { previous = TestcontainersConfiguration.getInstance(); } REF.set(Mockito.spy(previous)); context.getStore(NS).put(context.getUniqueId(), previous); } @Override public void afterEach(ExtensionContext context) throws Exception { TestcontainersConfiguration previous = context .getStore(NS) .remove(context.getUniqueId(), TestcontainersConfiguration.class); REF.set(previous); } } ================================================ FILE: core/src/test/java/org/testcontainers/utility/MountableFileTest.java ================================================ package org.testcontainers.utility; import lombok.Cleanup; import org.apache.commons.compress.archivers.ArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.function.Consumer; import static org.assertj.core.api.Assertions.assertThat; class MountableFileTest { private static final int TEST_FILE_MODE = 0532; private static final int BASE_FILE_MODE = 0100000; private static final int BASE_DIR_MODE = 0040000; @Test void forClasspathResource() throws Exception { final MountableFile mountableFile = MountableFile.forClasspathResource("mappable-resource/test-resource.txt"); performChecks(mountableFile); } @Test void forClasspathResourceWithAbsolutePath() throws Exception { final MountableFile mountableFile = MountableFile.forClasspathResource("/mappable-resource/test-resource.txt"); performChecks(mountableFile); } @Test void forClasspathResourceFromJar() throws Exception { final MountableFile mountableFile = MountableFile.forClasspathResource("META-INF/dummy_unique_name.txt"); performChecks(mountableFile); } @Test void forClasspathResourceFromJarWithAbsolutePath() throws Exception { final MountableFile mountableFile = MountableFile.forClasspathResource("/META-INF/dummy_unique_name.txt"); performChecks(mountableFile); } @Test void forHostPath() throws Exception { final Path file = createTempFile("somepath"); final MountableFile mountableFile = MountableFile.forHostPath(file.toString()); performChecks(mountableFile); } @Test void forHostPathWithSpaces() throws Exception { final Path file = createTempFile("some path"); final MountableFile mountableFile = MountableFile.forHostPath(file.toString()); performChecks(mountableFile); assertThat(mountableFile.getResolvedPath()).as("The resolved path contains the original space").contains(" "); assertThat(mountableFile.getResolvedPath()) .as("The resolved path does not contain an escaped space") .doesNotContain("\\ "); } @Test void forHostPathWithPlus() throws Exception { final Path file = createTempFile("some+path"); final MountableFile mountableFile = MountableFile.forHostPath(file.toString()); performChecks(mountableFile); assertThat(mountableFile.getResolvedPath()).as("The resolved path contains the original space").contains("+"); assertThat(mountableFile.getResolvedPath()) .as("The resolved path does not contain an escaped space") .doesNotContain(" "); } @Test void forClasspathResourceWithPermission() throws Exception { final MountableFile mountableFile = MountableFile.forClasspathResource( "mappable-resource/test-resource.txt", TEST_FILE_MODE ); performChecks(mountableFile); assertThat(mountableFile.getFileMode()).as("Valid file mode.").isEqualTo(BASE_FILE_MODE | TEST_FILE_MODE); } @Test void forHostFilePathWithPermission() throws Exception { final Path file = createTempFile("somepath"); final MountableFile mountableFile = MountableFile.forHostPath(file.toString(), TEST_FILE_MODE); performChecks(mountableFile); assertThat(mountableFile.getFileMode()).as("Valid file mode.").isEqualTo(BASE_FILE_MODE | TEST_FILE_MODE); } @Test void forHostDirPathWithPermission() throws Exception { final Path dir = createTempDir(); final MountableFile mountableFile = MountableFile.forHostPath(dir.toString(), TEST_FILE_MODE); performChecks(mountableFile); assertThat(mountableFile.getFileMode()).as("Valid dir mode.").isEqualTo(BASE_DIR_MODE | TEST_FILE_MODE); } @Test void noTrailingSlashesInTarEntryNames() throws Exception { final MountableFile mountableFile = MountableFile.forClasspathResource("mappable-resource/test-resource.txt"); @Cleanup final TarArchiveInputStream tais = intoTarArchive(taos -> { mountableFile.transferTo(taos, "/some/path.txt"); mountableFile.transferTo(taos, "/path.txt"); mountableFile.transferTo(taos, "path.txt"); }); ArchiveEntry entry; while ((entry = tais.getNextEntry()) != null) { assertThat(entry.getName()).as("no entries should have a trailing slash").doesNotEndWith("/"); } } private TarArchiveInputStream intoTarArchive(Consumer consumer) throws IOException { @Cleanup final ByteArrayOutputStream baos = new ByteArrayOutputStream(); @Cleanup final TarArchiveOutputStream taos = new TarArchiveOutputStream(baos); consumer.accept(taos); taos.close(); return new TarArchiveInputStream(new ByteArrayInputStream(baos.toByteArray())); } @SuppressWarnings("ResultOfMethodCallIgnored") @NotNull private Path createTempFile(final String name) throws IOException { final File tempParentDir = File.createTempFile("testcontainers", ""); tempParentDir.delete(); tempParentDir.mkdirs(); final Path file = new File(tempParentDir, name).toPath(); Files.copy(MountableFileTest.class.getResourceAsStream("/mappable-resource/test-resource.txt"), file); return file; } @NotNull private Path createTempDir() throws IOException { return Files.createTempDirectory("testcontainers"); } private void performChecks(final MountableFile mountableFile) { final String mountablePath = mountableFile.getResolvedPath(); assertThat(new File(mountablePath)).as("The filesystem path '" + mountablePath + "' can be found").exists(); assertThat(mountablePath) .as("The filesystem path '" + mountablePath + "' does not contain any URL escaping") .doesNotContain("%20"); } } ================================================ FILE: core/src/test/java/org/testcontainers/utility/PrefixingImageNameSubstitutorTest.java ================================================ package org.testcontainers.utility; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class PrefixingImageNameSubstitutorTest { private TestcontainersConfiguration mockConfiguration; private PrefixingImageNameSubstitutor underTest; @BeforeEach public void setUp() { mockConfiguration = mock(TestcontainersConfiguration.class); underTest = new PrefixingImageNameSubstitutor(mockConfiguration); } @Test void testHappyPath() { when(mockConfiguration.getEnvVarOrProperty(eq(PrefixingImageNameSubstitutor.PREFIX_PROPERTY_KEY), any())) .thenReturn("someregistry.com/our-mirror/"); final DockerImageName result = underTest.apply(DockerImageName.parse("some/image:tag")); assertThat(result.asCanonicalNameString()) .as("The prefix is applied") .isEqualTo("someregistry.com/our-mirror/some/image:tag"); } @Test void hubIoRegistryIsNotChanged() { when(mockConfiguration.getEnvVarOrProperty(eq(PrefixingImageNameSubstitutor.PREFIX_PROPERTY_KEY), any())) .thenReturn("someregistry.com/our-mirror/"); final DockerImageName result = underTest.apply(DockerImageName.parse("docker.io/some/image:tag")); assertThat(result.asCanonicalNameString()).as("The prefix is applied").isEqualTo("docker.io/some/image:tag"); } @Test void hubComRegistryIsNotChanged() { when(mockConfiguration.getEnvVarOrProperty(eq(PrefixingImageNameSubstitutor.PREFIX_PROPERTY_KEY), any())) .thenReturn("someregistry.com/our-mirror/"); final DockerImageName result = underTest.apply(DockerImageName.parse("registry.hub.docker.com/some/image:tag")); assertThat(result.asCanonicalNameString()) .as("The prefix is applied") .isEqualTo("registry.hub.docker.com/some/image:tag"); } @Test void thirdPartyRegistriesNotAffected() { when(mockConfiguration.getEnvVarOrProperty(eq(PrefixingImageNameSubstitutor.PREFIX_PROPERTY_KEY), any())) .thenReturn("someregistry.com/our-mirror/"); final DockerImageName result = underTest.apply(DockerImageName.parse("gcr.io/something/image:tag")); assertThat(result.asCanonicalNameString()) .as("The prefix is not applied if a third party registry is used") .isEqualTo("gcr.io/something/image:tag"); } @Test void testNoDoublePrefixing() { when(mockConfiguration.getEnvVarOrProperty(eq(PrefixingImageNameSubstitutor.PREFIX_PROPERTY_KEY), any())) .thenReturn("someregistry.com/our-mirror/"); final DockerImageName result = underTest.apply(DockerImageName.parse("someregistry.com/some/image:tag")); assertThat(result.asCanonicalNameString()) .as("The prefix is not applied if already present") .isEqualTo("someregistry.com/some/image:tag"); } @Test void testHandlesEmptyValue() { when(mockConfiguration.getEnvVarOrProperty(eq(PrefixingImageNameSubstitutor.PREFIX_PROPERTY_KEY), any())) .thenReturn(""); final DockerImageName result = underTest.apply(DockerImageName.parse("some/image:tag")); assertThat(result.asCanonicalNameString()) .as("The prefix is not applied if the env var is not set") .isEqualTo("some/image:tag"); } @Test void testHandlesRegistryOnlyWithTrailingSlash() { when(mockConfiguration.getEnvVarOrProperty(eq(PrefixingImageNameSubstitutor.PREFIX_PROPERTY_KEY), any())) .thenReturn("someregistry.com/"); final DockerImageName result = underTest.apply(DockerImageName.parse("some/image:tag")); assertThat(result.asCanonicalNameString()) .as("The prefix is applied") .isEqualTo("someregistry.com/some/image:tag"); } @Test void testCombinesLiterallyForRegistryOnlyWithoutTrailingSlash() { when(mockConfiguration.getEnvVarOrProperty(eq(PrefixingImageNameSubstitutor.PREFIX_PROPERTY_KEY), any())) .thenReturn("someregistry.com"); final DockerImageName result = underTest.apply(DockerImageName.parse("some/image:tag")); assertThat(result.asCanonicalNameString()) .as("The prefix is applied") .isEqualTo("someregistry.comsome/image:tag"); } @Test void testCombinesLiterallyForBothPartsWithoutTrailingSlash() { when(mockConfiguration.getEnvVarOrProperty(eq(PrefixingImageNameSubstitutor.PREFIX_PROPERTY_KEY), any())) .thenReturn("someregistry.com/our-mirror"); final DockerImageName result = underTest.apply(DockerImageName.parse("some/image:tag")); assertThat(result.asCanonicalNameString()) .as("The prefix is applied") .isEqualTo("someregistry.com/our-mirrorsome/image:tag"); } } ================================================ FILE: core/src/test/java/org/testcontainers/utility/RegistryAuthLocatorTest.java ================================================ package org.testcontainers.utility; import com.github.dockerjava.api.model.AuthConfig; import com.google.common.io.Resources; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.SystemUtils; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; class RegistryAuthLocatorTest { @Test void lookupAuthConfigWithoutCredentials() throws URISyntaxException, IOException { final RegistryAuthLocator authLocator = createTestAuthLocator("config-empty.json"); final AuthConfig authConfig = authLocator.lookupAuthConfig( DockerImageName.parse("unauthenticated.registry.org/org/repo"), new AuthConfig() ); assertThat(authConfig.getRegistryAddress()) .as("Default docker registry URL is set on auth config") .isEqualTo("https://index.docker.io/v1/"); assertThat(authConfig.getUsername()).as("No username is set").isNull(); assertThat(authConfig.getPassword()).as("No password is set").isNull(); } @Test void lookupAuthConfigWithBasicAuthCredentials() throws URISyntaxException, IOException { final RegistryAuthLocator authLocator = createTestAuthLocator("config-basic-auth.json"); final AuthConfig authConfig = authLocator.lookupAuthConfig( DockerImageName.parse("registry.example.com/org/repo"), new AuthConfig() ); assertThat(authConfig.getRegistryAddress()) .as("Default docker registry URL is set on auth config") .isEqualTo("https://registry.example.com"); assertThat(authConfig.getUsername()).as("Username is set").isEqualTo("user"); assertThat(authConfig.getPassword()).as("Password is set").isEqualTo("pass"); } @Test void lookupAuthConfigWithJsonKeyCredentials() throws URISyntaxException, IOException { final RegistryAuthLocator authLocator = createTestAuthLocator("config-with-json-key.json"); final AuthConfig authConfig = authLocator.lookupAuthConfig( DockerImageName.parse("registry.example.com/org/repo"), new AuthConfig() ); assertThat(authConfig.getRegistryAddress()) .as("Default docker registry URL is set on auth config") .isEqualTo("https://registry.example.com"); assertThat(authConfig.getUsername()).as("Username is set").isEqualTo("_json_key"); assertThat(authConfig.getPassword()).as("Password is set").isNotNull(); } @Test void lookupAuthConfigWithJsonKeyCredentialsPartialMatchShouldGiveNoResult() throws URISyntaxException, IOException { // contains entry for registry.example.com final RegistryAuthLocator authLocator = createTestAuthLocator("config-with-json-key.json"); final AuthConfig authConfig = authLocator.lookupAuthConfig( DockerImageName.parse("registry.example.co/org/repo"), // partial match of registry name new AuthConfig() ); assertThat(authConfig.getUsername()).as("auth config username").isNull(); assertThat(authConfig.getPassword()).as("auth config password").isNull(); } @Test void lookupAuthConfigUsingStore() throws URISyntaxException, IOException { final RegistryAuthLocator authLocator = createTestAuthLocator("config-with-store.json"); final AuthConfig authConfig = authLocator.lookupAuthConfig( DockerImageName.parse("registry.example.com/org/repo"), new AuthConfig() ); assertThat(authConfig.getRegistryAddress()) .as("Correct server URL is obtained from a credential store") .isEqualTo("url"); assertThat(authConfig.getUsername()) .as("Correct username is obtained from a credential store") .isEqualTo("username"); assertThat(authConfig.getPassword()) .as("Correct secret is obtained from a credential store") .isEqualTo("secret"); } @Test void lookupAuthConfigUsingHelper() throws URISyntaxException, IOException { final RegistryAuthLocator authLocator = createTestAuthLocator("config-with-helper.json"); final AuthConfig authConfig = authLocator.lookupAuthConfig( DockerImageName.parse("registry.example.com/org/repo"), new AuthConfig() ); assertThat(authConfig.getRegistryAddress()) .as("Correct server URL is obtained from a credential store") .isEqualTo("url"); assertThat(authConfig.getUsername()) .as("Correct username is obtained from a credential store") .isEqualTo("username"); assertThat(authConfig.getPassword()) .as("Correct secret is obtained from a credential store") .isEqualTo("secret"); } @Test void lookupAuthConfigUsingHelperWithToken() throws URISyntaxException, IOException { final RegistryAuthLocator authLocator = createTestAuthLocator("config-with-helper-using-token.json"); final AuthConfig authConfig = authLocator.lookupAuthConfig( DockerImageName.parse("registrytoken.example.com/org/repo"), new AuthConfig() ); assertThat(authConfig.getRegistryAddress()) .as("Correct server URL is obtained from a credential store") .isEqualTo("url"); assertThat(authConfig.getIdentitytoken()) .as("Correct identitytoken is obtained from a credential store") .isEqualTo("secret"); } @Test void lookupUsingHelperEmptyAuth() throws URISyntaxException, IOException { final RegistryAuthLocator authLocator = createTestAuthLocator("config-empty-auth-with-helper.json"); final AuthConfig authConfig = authLocator.lookupAuthConfig( DockerImageName.parse("registry.example.com/org/repo"), new AuthConfig() ); assertThat(authConfig.getRegistryAddress()) .as("Correct server URL is obtained from a credential store") .isEqualTo("url"); assertThat(authConfig.getUsername()) .as("Correct username is obtained from a credential store") .isEqualTo("username"); assertThat(authConfig.getPassword()) .as("Correct secret is obtained from a credential store") .isEqualTo("secret"); } @Test void lookupNonEmptyAuthWithHelper() throws URISyntaxException, IOException { final RegistryAuthLocator authLocator = createTestAuthLocator("config-existing-auth-with-helper.json"); final AuthConfig authConfig = authLocator.lookupAuthConfig( DockerImageName.parse("registry.example.com/org/repo"), new AuthConfig() ); assertThat(authConfig.getRegistryAddress()) .as("Correct server URL is obtained from a credential helper") .isEqualTo("url"); assertThat(authConfig.getUsername()) .as("Correct username is obtained from a credential helper") .isEqualTo("username"); assertThat(authConfig.getPassword()) .as("Correct password is obtained from a credential helper") .isEqualTo("secret"); } @Test void lookupAuthConfigUsingHelperNoServerUrl() throws URISyntaxException, IOException { final RegistryAuthLocator authLocator = createTestAuthLocator("config-with-helper-no-server-url.json"); final AuthConfig authConfig = authLocator.lookupAuthConfig( DockerImageName.parse("registrynoserverurl.example.com/org/repo"), new AuthConfig() ); assertThat(authConfig.getRegistryAddress()) .as("Fallback (registry) server URL is used") .isEqualTo("registrynoserverurl.example.com"); assertThat(authConfig.getUsername()) .as("Correct username is obtained from a credential store") .isEqualTo("username"); assertThat(authConfig.getPassword()) .as("Correct secret is obtained from a credential store") .isEqualTo("secret"); } @Test void lookupAuthConfigUsingHelperNoServerUrlWithToken() throws URISyntaxException, IOException { final RegistryAuthLocator authLocator = createTestAuthLocator( "config-with-helper-no-server-url-using-token.json" ); final AuthConfig authConfig = authLocator.lookupAuthConfig( DockerImageName.parse("registrynoserverurltoken.example.com/org/repo"), new AuthConfig() ); assertThat(authConfig.getRegistryAddress()) .as("Fallback (registry) server URL is used") .isEqualTo("registrynoserverurltoken.example.com"); assertThat(authConfig.getIdentitytoken()) .as("Correct identitytoken is obtained from a credential store") .isEqualTo("secret"); } @Test void lookupAuthConfigWithCredentialsNotFound() throws URISyntaxException, IOException { Map notFoundMessagesReference = new HashMap<>(); final RegistryAuthLocator authLocator = createTestAuthLocator( "config-with-store.json", notFoundMessagesReference ); DockerImageName dockerImageName = DockerImageName.parse("registry2.example.com/org/repo"); final AuthConfig authConfig = authLocator.lookupAuthConfig(dockerImageName, new AuthConfig()); assertThat(authConfig.getUsername()) .as("No username should have been obtained from a credential store") .isNull(); assertThat(authConfig.getPassword()).as("No secret should have been obtained from a credential store").isNull(); assertThat(notFoundMessagesReference.size()) .as("Should have one 'credentials not found' message discovered") .isEqualTo(1); String discoveredMessage = notFoundMessagesReference.values().iterator().next(); assertThat(discoveredMessage) .as("Not correct message discovered") .isEqualTo("Fake credentials not found on credentials store 'registry2.example.com'"); } @Test void lookupAuthConfigWithCredStoreEmpty() throws URISyntaxException, IOException { final RegistryAuthLocator authLocator = createTestAuthLocator("config-with-store-empty.json"); DockerImageName dockerImageName = DockerImageName.parse("registry2.example.com/org/repo"); final AuthConfig authConfig = authLocator.lookupAuthConfig(dockerImageName, new AuthConfig()); assertThat(authConfig.getAuth()).as("CredStore field will be ignored, because value is blank").isNull(); } @Test void lookupAuthConfigFromEnvVarWithCredStoreEmpty() throws URISyntaxException, IOException { final RegistryAuthLocator authLocator = createTestAuthLocator(null, "config-with-store-empty.json"); DockerImageName dockerImageName = DockerImageName.parse("registry2.example.com/org/repo"); final AuthConfig authConfig = authLocator.lookupAuthConfig(dockerImageName, new AuthConfig()); assertThat(authConfig.getAuth()).as("CredStore field will be ignored, because value is blank").isNull(); } @Test void lookupAuthConfigWithoutConfigFile() throws URISyntaxException, IOException { final RegistryAuthLocator authLocator = createTestAuthLocator(null); final AuthConfig authConfig = authLocator.lookupAuthConfig( DockerImageName.parse("unauthenticated.registry.org/org/repo"), new AuthConfig() ); assertThat(authConfig.getRegistryAddress()) .as("Default docker registry URL is set on auth config") .isEqualTo("https://index.docker.io/v1/"); assertThat(authConfig.getUsername()).as("No username is set").isNull(); assertThat(authConfig.getPassword()).as("No password is set").isNull(); } @Test void lookupAuthConfigRespectsCheckOrderPreference() throws URISyntaxException, IOException { final RegistryAuthLocator authLocator = createTestAuthLocator("config-empty.json", "config-basic-auth.json"); final AuthConfig authConfig = authLocator.lookupAuthConfig( DockerImageName.parse("registry.example.com/org/repo"), new AuthConfig() ); assertThat(authConfig.getRegistryAddress()) .as("Default docker registry URL is set on auth config") .isEqualTo("https://registry.example.com"); assertThat(authConfig.getUsername()).as("Username is set").isEqualTo("user"); assertThat(authConfig.getPassword()).as("Password is set").isEqualTo("pass"); } @Test void lookupAuthConfigFromEnvironmentVariable() throws URISyntaxException, IOException { final RegistryAuthLocator authLocator = createTestAuthLocator(null, "config-basic-auth.json"); final AuthConfig authConfig = authLocator.lookupAuthConfig( DockerImageName.parse("registry.example.com/org/repo"), new AuthConfig() ); assertThat(authConfig.getRegistryAddress()) .as("Default docker registry URL is set on auth config") .isEqualTo("https://registry.example.com"); assertThat(authConfig.getUsername()).as("Username is set").isEqualTo("user"); assertThat(authConfig.getPassword()).as("Password is set").isEqualTo("pass"); } @NotNull private RegistryAuthLocator createTestAuthLocator(String configName, String envConfigName) throws URISyntaxException, IOException { return createTestAuthLocator(configName, envConfigName, new HashMap<>()); } @NotNull private RegistryAuthLocator createTestAuthLocator(String configName) throws URISyntaxException, IOException { return createTestAuthLocator(configName, null, new HashMap<>()); } @NotNull private RegistryAuthLocator createTestAuthLocator(String configName, Map notFoundMessagesReference) throws URISyntaxException, IOException { return createTestAuthLocator(configName, null, notFoundMessagesReference); } @NotNull private RegistryAuthLocator createTestAuthLocator( String configName, String envConfigName, Map notFoundMessagesReference ) throws URISyntaxException, IOException { File configFile = null; String commandPathPrefix = ""; String commandExtension = ""; String configEnv = null; if (configName != null) { configFile = new File(Resources.getResource("auth-config/" + configName).toURI()); commandPathPrefix = configFile.getParentFile().getAbsolutePath() + "/"; } else { configFile = new File(new URI("file:///not-exists.json")); } if (envConfigName != null) { final File envConfigFile = new File(Resources.getResource("auth-config/" + envConfigName).toURI()); configEnv = FileUtils.readFileToString(envConfigFile, StandardCharsets.UTF_8); commandPathPrefix = envConfigFile.getParentFile().getAbsolutePath() + "/"; } if (SystemUtils.IS_OS_WINDOWS) { commandPathPrefix += "win/"; // need to provide executable extension otherwise won't run it // with real docker wincredential exe there is no problem commandExtension = ".bat"; } return new RegistryAuthLocator( configFile, configEnv, commandPathPrefix, commandExtension, notFoundMessagesReference ); } } ================================================ FILE: core/src/test/java/org/testcontainers/utility/ResourceReaperTest.java ================================================ package org.testcontainers.utility; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.dockerjava.api.DockerClient; import lombok.SneakyThrows; import org.awaitility.Awaitility; import org.awaitility.core.ConditionFactory; import org.junit.jupiter.api.Test; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.GenericContainer; import org.zeroturnaround.exec.ProcessExecutor; import org.zeroturnaround.exec.ProcessResult; import java.io.File; import java.time.Duration; import java.util.List; import java.util.Map; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; class ResourceReaperTest { @Test void shouldCleanupWithRyuk() { Map labels = runProcess(processExecutor -> {}); assertCleanup(labels); } @Test void shouldCleanupWithJVM() { Map labels = runProcess(processExecutor -> { processExecutor.environment("TESTCONTAINERS_RYUK_DISABLED", "true"); }); assertCleanup(labels); } private void assertCleanup(Map labels) { DockerClient client = DockerClientFactory.instance().client(); ConditionFactory awaitFactory = Awaitility .await() .atMost(Duration.ofMinutes(1)) .pollInterval(Duration.ofSeconds(1)); List labelValues = labels .entrySet() .stream() .map(it -> it.getKey() + "=" + it.getValue()) .collect(Collectors.toList()); awaitFactory.untilAsserted(() -> { assertThat(client.listContainersCmd().withFilter("label", labelValues).withShowAll(true).exec()).isEmpty(); }); awaitFactory.untilAsserted(() -> { assertThat(client.listNetworksCmd().withFilter("label", labelValues).exec()).isEmpty(); }); awaitFactory.untilAsserted(() -> { assertThat(client.listVolumesCmd().withFilter("label", labelValues).exec().getVolumes()).isEmpty(); }); } @SneakyThrows private Map runProcess(Consumer processExecutorConsumer) { ProcessExecutor processExecutor = new ProcessExecutor( new File(System.getProperty("java.home")).toPath().resolve("bin").resolve("java").toString(), "-ea", "-classpath", System.getProperty("java.class.path"), SimpleUsage.class.getName() ); processExecutor.readOutput(true); processExecutor.redirectOutput(System.out); processExecutor.redirectError(System.err); processExecutorConsumer.accept(processExecutor); ProcessResult result = processExecutor.execute(); assertThat(result.getExitValue()).isEqualTo(0); String labelsJson = Stream .of(result.outputUTF8().split("\n")) .filter(it -> it.startsWith(SimpleUsage.LABELS_MARKER)) .map(it -> it.substring(SimpleUsage.LABELS_MARKER.length())) .findFirst() .get(); return new ObjectMapper().readValue(labelsJson, Map.class); } public static class SimpleUsage { static final String LABELS_MARKER = "LABELS:"; @SneakyThrows @SuppressWarnings("deprecation") public static void main(String[] args) { System.out.println( LABELS_MARKER + new ObjectMapper().writeValueAsString(ResourceReaper.instance().getLabels()) ); GenericContainer container = new GenericContainer<>("testcontainers/helloworld:1.1.0") .withNetwork(org.testcontainers.containers.Network.newNetwork()) .withExposedPorts(8080); container.start(); } } } ================================================ FILE: core/src/test/java/org/testcontainers/utility/TestEnvironmentTest.java ================================================ package org.testcontainers.utility; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * Created by rnorth on 03/07/2016. */ class TestEnvironmentTest { @Test void testCompareVersionGreaterThanSameMajor() { assertThat(new ComparableVersion("1.22").compareTo(new ComparableVersion("1.20")) > 0) .as("1.22 > 1.20") .isTrue(); } @Test void testCompareVersionEqual() { assertThat(new ComparableVersion("1.20")) .as("1.20 == 1.20") .isEqualByComparingTo(new ComparableVersion("1.20")); } @Test void testCompareVersionGreaterThan() { assertThat(new ComparableVersion("2.10").compareTo(new ComparableVersion("1.20")) > 0) .as("2.10 > 1.20") .isTrue(); } @Test void testCompareVersionIgnoresExcessLength() { assertThat(new ComparableVersion("1.20")) .as("1.20 == 1.20.3") .isEqualByComparingTo(new ComparableVersion("1.20.3")); } } ================================================ FILE: core/src/test/java/org/testcontainers/utility/TestcontainersConfigurationTest.java ================================================ package org.testcontainers.utility; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; class TestcontainersConfigurationTest { private Properties userProperties; private Properties classpathProperties; private Map environment; @BeforeEach public void setUp() { userProperties = new Properties(); classpathProperties = new Properties(); environment = new HashMap<>(); } @Test void shouldSubstituteImageNamesFromClasspathProperties() { classpathProperties.setProperty("ryuk.container.image", "foo:version"); assertThat(newConfig().getConfiguredSubstituteImage(DockerImageName.parse("testcontainers/ryuk:any"))) .as("an image name can be pulled from classpath properties") .isEqualTo(DockerImageName.parse("foo:version")); } @Test void shouldSubstituteImageNamesFromUserProperties() { userProperties.setProperty("ryuk.container.image", "foo:version"); assertThat(newConfig().getConfiguredSubstituteImage(DockerImageName.parse("testcontainers/ryuk:any"))) .as("an image name can be pulled from user properties") .isEqualTo(DockerImageName.parse("foo:version")); } @Test void shouldSubstituteImageNamesFromEnvironmentVariables() { environment.put("TESTCONTAINERS_RYUK_CONTAINER_IMAGE", "foo:version"); assertThat(newConfig().getConfiguredSubstituteImage(DockerImageName.parse("testcontainers/ryuk:any"))) .as("an image name can be pulled from an environment variable") .isEqualTo(DockerImageName.parse("foo:version")); } @Test void shouldApplySettingsInOrder() { assertThat(newConfig().getEnvVarOrProperty("key", "default")) .as("precedence order for multiple sources of the same value is correct") .isEqualTo("default"); classpathProperties.setProperty("key", "foo"); assertThat(newConfig().getEnvVarOrProperty("key", "default")) .as("precedence order for multiple sources of the same value is correct") .isEqualTo("foo"); userProperties.setProperty("key", "bar"); assertThat(newConfig().getEnvVarOrProperty("key", "default")) .as("precedence order for multiple sources of the same value is correct") .isEqualTo("bar"); environment.put("TESTCONTAINERS_KEY", "baz"); assertThat(newConfig().getEnvVarOrProperty("key", "default")) .as("precedence order for multiple sources of the same value is correct") .isEqualTo("baz"); } @Test void shouldNotReadChecksFromClasspathProperties() { assertThat(newConfig().isDisableChecks()).as("checks enabled by default").isFalse(); classpathProperties.setProperty("checks.disable", "true"); assertThat(newConfig().isDisableChecks()).as("checks are not affected by classpath properties").isFalse(); } @Test void shouldReadChecksFromUserProperties() { assertThat(newConfig().isDisableChecks()).as("checks enabled by default").isFalse(); userProperties.setProperty("checks.disable", "true"); assertThat(newConfig().isDisableChecks()).as("checks disabled via user properties").isTrue(); } @Test void shouldReadChecksFromEnvironment() { assertThat(newConfig().isDisableChecks()).as("checks enabled by default").isFalse(); userProperties.remove("checks.disable"); environment.put("TESTCONTAINERS_CHECKS_DISABLE", "true"); assertThat(newConfig().isDisableChecks()).as("checks disabled via env var").isTrue(); } @Test void shouldReadDockerSettingsFromEnvironmentWithoutTestcontainersPrefix() { userProperties.remove("docker.foo"); environment.put("DOCKER_FOO", "some value"); assertThat(newConfig().getEnvVarOrUserProperty("docker.foo", "default")) .as("reads unprefixed env vars for docker. settings") .isEqualTo("some value"); } @Test void shouldNotReadDockerSettingsFromEnvironmentWithTestcontainersPrefix() { userProperties.remove("docker.foo"); environment.put("TESTCONTAINERS_DOCKER_FOO", "some value"); assertThat(newConfig().getEnvVarOrUserProperty("docker.foo", "default")) .as("reads unprefixed env vars for docker. settings") .isEqualTo("default"); } @Test void shouldReadDockerSettingsFromUserProperties() { environment.remove("DOCKER_FOO"); userProperties.put("docker.foo", "some value"); assertThat(newConfig().getEnvVarOrUserProperty("docker.foo", "default")) .as("reads unprefixed user properties for docker. settings") .isEqualTo("some value"); } @Test void shouldNotReadSettingIfCorrespondingEnvironmentVarIsEmptyString() { environment.put("DOCKER_FOO", ""); assertThat(newConfig().getEnvVarOrUserProperty("docker.foo", "default")) .as("reads unprefixed env vars for docker. settings") .isEqualTo("default"); } @Test void shouldNotReadDockerClientStrategyFromClasspathProperties() { String currentValue = newConfig().getDockerClientStrategyClassName(); classpathProperties.setProperty("docker.client.strategy", UUID.randomUUID().toString()); assertThat(newConfig().getDockerClientStrategyClassName()) .as("Docker client strategy is not affected by classpath properties") .isEqualTo(currentValue); } @Test void shouldReadDockerClientStrategyFromUserProperties() { userProperties.setProperty("docker.client.strategy", "foo"); assertThat(newConfig().getDockerClientStrategyClassName()) .as("Docker client strategy is changed by user property") .isEqualTo("foo"); } @Test void shouldReadDockerClientStrategyFromEnvironment() { userProperties.remove("docker.client.strategy"); environment.put("TESTCONTAINERS_DOCKER_CLIENT_STRATEGY", "foo"); assertThat(newConfig().getDockerClientStrategyClassName()) .as("Docker client strategy is changed by env var") .isEqualTo("foo"); } @Test void shouldNotUseImplicitDockerClientStrategyWhenDockerHostAndStrategyAreBothSet() { userProperties.put("docker.client.strategy", "foo"); userProperties.put("docker.host", "tcp://1.2.3.4:5678"); assertThat(newConfig().getDockerClientStrategyClassName()) .as("Docker client strategy is can be explicitly set") .isEqualTo("foo"); userProperties.remove("docker.client.strategy"); environment.put("TESTCONTAINERS_DOCKER_CLIENT_STRATEGY", "bar"); userProperties.put("docker.client.strategy", "foo"); assertThat(newConfig().getDockerClientStrategyClassName()) .as("Docker client strategy is can be explicitly set") .isEqualTo("bar"); environment.put("TESTCONTAINERS_DOCKER_CLIENT_STRATEGY", "bar"); userProperties.remove("docker.client.strategy"); assertThat(newConfig().getDockerClientStrategyClassName()) .as("Docker client strategy is can be explicitly set") .isEqualTo("bar"); environment.remove("TESTCONTAINERS_DOCKER_CLIENT_STRATEGY"); userProperties.put("docker.client.strategy", "foo"); assertThat(newConfig().getDockerClientStrategyClassName()) .as("Docker client strategy is can be explicitly set") .isEqualTo("foo"); } @Test void shouldNotReadReuseFromClasspathProperties() { assertThat(newConfig().environmentSupportsReuse()).as("no reuse by default").isFalse(); classpathProperties.setProperty("testcontainers.reuse.enable", "true"); assertThat(newConfig().environmentSupportsReuse()) .as("reuse is not affected by classpath properties") .isFalse(); } @Test void shouldReadReuseFromUserProperties() { assertThat(newConfig().environmentSupportsReuse()).as("no reuse by default").isFalse(); userProperties.setProperty("testcontainers.reuse.enable", "true"); assertThat(newConfig().environmentSupportsReuse()).as("reuse enabled via user property").isTrue(); } @Test void shouldReadReuseFromEnvironment() { assertThat(newConfig().environmentSupportsReuse()).as("no reuse by default").isFalse(); userProperties.remove("testcontainers.reuse.enable"); environment.put("TESTCONTAINERS_REUSE_ENABLE", "true"); assertThat(newConfig().environmentSupportsReuse()).as("reuse enabled via env var").isTrue(); } @Test void shouldTrimImageNames() { userProperties.setProperty("ryuk.container.image", " testcontainers/ryuk:0.3.2 "); assertThat(newConfig().getRyukImage()) .as("trailing whitespace was not removed from image name property") .isEqualTo("testcontainers/ryuk:0.3.2"); } private TestcontainersConfiguration newConfig() { return new TestcontainersConfiguration(userProperties, classpathProperties, environment); } } ================================================ FILE: core/src/test/resources/Dockerfile ================================================ FROM postgres ================================================ FILE: core/src/test/resources/Dockerfile-multistage ================================================ FROM alpine:3.14 AS builder WORKDIR /my-files RUN echo 'Hello World' > hello.txt FROM alpine:3.14 COPY --from=builder /my-files/hello.txt hello.txt ================================================ FILE: core/src/test/resources/META-INF/services/org.testcontainers.core.CreateContainerCmdModifier ================================================ org.testcontainers.custom.TestCreateContainerCmdModifier ================================================ FILE: core/src/test/resources/auth-config/config-basic-auth.json ================================================ { "auths": { "https://registry.example.com": { "email": "user@example.com", "auth": "dXNlcjpwYXNz" } } } ================================================ FILE: core/src/test/resources/auth-config/config-empty-auth-with-helper.json ================================================ { "auths": { }, "HttpHeaders": { "User-Agent": "Docker-Client/18.03.0-ce (darwin)" }, "credHelpers": { "registry.example.com": "fake" } } ================================================ FILE: core/src/test/resources/auth-config/config-empty.json ================================================ { "HttpHeaders": { "User-Agent": "Docker-Client/18.03.0-ce (darwin)" } } ================================================ FILE: core/src/test/resources/auth-config/config-existing-auth-with-helper.json ================================================ { "auths": { "https://registry.example.com": { "email": "not@val.id", "auth": "dXNlcjpwYXNz" } }, "HttpHeaders": { "User-Agent": "Docker-Client/18.03.0-ce (darwin)" }, "credHelpers": { "registry.example.com": "fake" } } ================================================ FILE: core/src/test/resources/auth-config/config-with-helper-and-store.json ================================================ { "auths": { "registry.example.com": {} }, "HttpHeaders": { "User-Agent": "Docker-Client/18.03.0-ce (darwin)" }, "credsStore": "fake", "credHelpers": { "registry.example.com": "fake" } } ================================================ FILE: core/src/test/resources/auth-config/config-with-helper-no-server-url-using-token.json ================================================ { "auths": { "registrynoserverurltoken.example.com": {} }, "HttpHeaders": { "User-Agent": "Docker-Client/18.03.0-ce (darwin)" }, "credHelpers": { "registrynoserverurltoken.example.com": "fake" } } ================================================ FILE: core/src/test/resources/auth-config/config-with-helper-no-server-url.json ================================================ { "auths": { "registrynoserverurl.example.com": {} }, "HttpHeaders": { "User-Agent": "Docker-Client/18.03.0-ce (darwin)" }, "credHelpers": { "registrynoserverurl.example.com": "fake" } } ================================================ FILE: core/src/test/resources/auth-config/config-with-helper-using-token.json ================================================ { "auths": { "registrytoken.example.com": {} }, "HttpHeaders": { "User-Agent": "Docker-Client/18.03.0-ce (darwin)" }, "credHelpers": { "registrytoken.example.com": "fake" } } ================================================ FILE: core/src/test/resources/auth-config/config-with-helper.json ================================================ { "auths": { "registry.example.com": {} }, "HttpHeaders": { "User-Agent": "Docker-Client/18.03.0-ce (darwin)" }, "credHelpers": { "registry.example.com": "fake" } } ================================================ FILE: core/src/test/resources/auth-config/config-with-json-key.json ================================================ { "auths": { "https://registry.example.com": { "auth": "X2pzb25fa2V5OnsKInR5cGUiOiAic2VydmljZV9hY2NvdW50IiwKInByb2plY3RfaWQiOiAiZXhhbXBsZS1wcm9qZWN0IiwKInByaXZhdGVfa2V5X2lkIjogImV4YW1wbGUta2V5LWlkIiwKInByaXZhdGVfa2V5IjogImV4YW1wbGUtcHJpdmF0ZS1rZXkiLAoiY2xpZW50X2VtYWlsIjogInVzZXJAZXhhbXBsZS5jb20iLAoiY2xpZW50X2lkIjogInVzZXJAZXhhbXBsZS5jb20iLAoiYXV0aF91cmkiOiAiaHR0cHM6Ly9yZWdpc3RyeS5leGFtcGxlLmNvbS9vL29hdXRoMi9hdXRoIiwKInRva2VuX3VyaSI6ICJodHRwczovL3JlZ2lzdHJ5LmV4YW1wbGUuY29tL3Rva2VuIiwKImF1dGhfcHJvdmlkZXJfeDUwOV9jZXJ0X3VybCI6ICJodHRwczovL3JlZ2lzdHJ5LmV4YW1wbGUuY29tIiwKImNsaWVudF94NTA5X2NlcnRfdXJsIjogImh0dHBzOi8vcmVnaXN0cnkuZXhhbXBsZS5jb20iCn0=" } } } ================================================ FILE: core/src/test/resources/auth-config/config-with-store-empty.json ================================================ { "auths": { "registry.example.com": {} }, "HttpHeaders": { "User-Agent": "Docker-Client/18.03.0-ce (darwin)" }, "credsStore": "" } ================================================ FILE: core/src/test/resources/auth-config/config-with-store.json ================================================ { "auths": { "registry.example.com": {} }, "HttpHeaders": { "User-Agent": "Docker-Client/18.03.0-ce (darwin)" }, "credsStore": "fake" } ================================================ FILE: core/src/test/resources/auth-config/docker-credential-fake ================================================ #!/bin/sh if [ $1 != "get" ]; then exit 1 fi read inputLine if [ "$inputLine" = "registry2.example.com" ]; then echo Fake credentials not found on credentials store \'$inputLine\' 0>&2 exit 1 fi if [ "$inputLine" = "registry.example.com" ]; then echo '{' \ ' "ServerURL": "url",' \ ' "Username": "username",' \ ' "Secret": "secret"' \ '}' exit 0 fi if [ "$inputLine" = "registrytoken.example.com" ]; then echo '{' \ ' "ServerURL": "url",' \ ' "Username": "",' \ ' "Secret": "secret"' \ '}' exit 0 fi if [ "$inputLine" = "registrynoserverurl.example.com" ]; then echo '{' \ ' "Username": "username",' \ ' "Secret": "secret"' \ '}' exit 0 fi if [ "$inputLine" = "registrynoserverurltoken.example.com" ]; then echo '{' \ ' "Username": "",' \ ' "Secret": "secret"' \ '}' exit 0 fi exit 1 ================================================ FILE: core/src/test/resources/auth-config/win/docker-credential-fake.bat ================================================ @echo off if not "%1" == "get" ( exit 1 ) set /p inputLine="" if "%inputLine%" == "registry2.example.com" ( echo Fake credentials not found on credentials store '%inputLine%' 0>&2 exit 1 ) if "%inputLine%" == "registry.example.com" ( echo { echo "ServerURL": "url", echo "Username": "username", echo "Secret": "secret" echo } exit 0 ) if "%inputLine%" == "registrytoken.example.com" ( echo { echo "ServerURL": "url", echo "Username": "", echo "Secret": "secret" echo } exit 0 ) if "%inputLine%" == "registrynoserverurl.example.com" ( echo { echo "Username": "username", echo "Secret": "secret" echo } exit 0 ) if "%inputLine%" == "registrynoserverurltoken.example.com" ( echo { echo "Username": "", echo "Secret": "secret" echo } exit 0 ) exit 1 ================================================ FILE: core/src/test/resources/compose-build-test/Dockerfile ================================================ FROM redis:6.0-alpine CMD ["redis-server"] ================================================ FILE: core/src/test/resources/compose-build-test/docker-compose.yml ================================================ version: '2.0' services: customredis: build: . normalredis: image: redis:6.0-alpine ================================================ FILE: core/src/test/resources/compose-dockerfile/Dockerfile ================================================ FROM alpine:3.17 ADD passthrough.sh /passthrough.sh CMD /passthrough.sh ================================================ FILE: core/src/test/resources/compose-dockerfile/passthrough.sh ================================================ #!/usr/bin/env sh while true; do echo "Exposing env on port 3000" env | nc -l -p 3000 done ================================================ FILE: core/src/test/resources/compose-file-copy-inclusions/Dockerfile ================================================ FROM jbangdev/jbang-action WORKDIR /app COPY EnvVariableRestEndpoint.java . RUN jbang export portable --force EnvVariableRestEndpoint.java EXPOSE 8080 CMD ["./EnvVariableRestEndpoint.java"] ================================================ FILE: core/src/test/resources/compose-file-copy-inclusions/EnvVariableRestEndpoint.java ================================================ ///usr/bin/env jbang "$0" "$@" ; exit $? import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpExchange; import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; public class EnvVariableRestEndpoint { private static final String ENV_VARIABLE_NAME = "MY_ENV_VARIABLE"; private static final int PORT = 8080; public static void main(String[] args) throws IOException { HttpServer server = HttpServer.create(new InetSocketAddress(PORT), 0); server.createContext("/env", new EnvVariableHandler()); server.setExecutor(null); server.start(); System.out.println("Server started on port " + PORT); } static class EnvVariableHandler implements HttpHandler { @Override public void handle(HttpExchange exchange) throws IOException { if ("GET".equals(exchange.getRequestMethod())) { String envValue = System.getenv(ENV_VARIABLE_NAME); String response = envValue != null ? ENV_VARIABLE_NAME + ": " + envValue : "Environment variable " + ENV_VARIABLE_NAME + " not found"; exchange.sendResponseHeaders(200, response.length()); try (OutputStream os = exchange.getResponseBody()) { os.write(response.getBytes()); } } else { String response = "Method not allowed"; exchange.sendResponseHeaders(405, response.length()); try (OutputStream os = exchange.getResponseBody()) { os.write(response.getBytes()); } } } } } ================================================ FILE: core/src/test/resources/compose-file-copy-inclusions/compose-root-only.yml ================================================ services: app: build: . ports: - "8080" env_file: - '.env' ================================================ FILE: core/src/test/resources/compose-file-copy-inclusions/compose-test-only.yml ================================================ services: app: build: . ports: - "8080" env_file: - './test/.env' ================================================ FILE: core/src/test/resources/compose-file-copy-inclusions/compose.yml ================================================ services: app: build: . ports: - "8080" env_file: - '.env' - './test/.env' ================================================ FILE: core/src/test/resources/compose-options-test/with-deploy-block.yml ================================================ version: '3.7' services: redis: image: redis:6-alpine deploy: resources: limits: memory: 150M ================================================ FILE: core/src/test/resources/compose-override/compose-override.yml ================================================ services: redis: image: redis:6-alpine ports: - 6379 environment: foo: !reset null ================================================ FILE: core/src/test/resources/compose-override/compose.yml ================================================ services: redis: image: redis:6-alpine ports: - 6379 environment: foo: bar ================================================ FILE: core/src/test/resources/compose-profile-option/compose-test.yml ================================================ services: redis: image: redis profiles: - cache db: image: mysql:8.0.36 environment: MYSQL_RANDOM_ROOT_PASSWORD: "true" profiles: - db ================================================ FILE: core/src/test/resources/compose-scaling-multiple-containers.yml ================================================ version: '2.4' services: redis: image: redis other: image: alpine:3.17 command: sleep 10000 ================================================ FILE: core/src/test/resources/compose-test.yml ================================================ redis: image: redis db: image: mysql:8.0.36 environment: MYSQL_RANDOM_ROOT_PASSWORD: "true" ================================================ FILE: core/src/test/resources/compose-v2-build-test/Dockerfile ================================================ FROM redis:7.0-alpine CMD ["redis-server"] ================================================ FILE: core/src/test/resources/compose-v2-build-test/docker-compose.yml ================================================ version: '2.0' services: customredis: build: . normalredis: image: redis:7.0-alpine ================================================ FILE: core/src/test/resources/compose-with-inline-scale-test.yml ================================================ version: '2.4' services: redis: image: redis scale: 3 # legacy mechanism to specify scale ================================================ FILE: core/src/test/resources/composev2/compose-test.yml ================================================ services: redis: image: redis db: image: mysql:8.0.36 environment: MYSQL_RANDOM_ROOT_PASSWORD: "true" ================================================ FILE: core/src/test/resources/composev2/scaled-compose-test.yml ================================================ services: redis: image: redis ================================================ FILE: core/src/test/resources/container-license-acceptance.txt ================================================ a b ================================================ FILE: core/src/test/resources/docker-compose-base.yml ================================================ version: '2.1' services: alpine: build: compose-dockerfile environment: bar: base ================================================ FILE: core/src/test/resources/docker-compose-container-name-v1.yml ================================================ redis: image: redis container_name: redis ================================================ FILE: core/src/test/resources/docker-compose-deserialization.yml ================================================ redis: image: redis key: !!org.testcontainers.containers.ParsedDockerComposeFileBean '{foo: bar}' ================================================ FILE: core/src/test/resources/docker-compose-healthcheck.yml ================================================ version: "3.9" services: redis: image: redis:alpine healthcheck: test: [ "CMD", "redis-cli", "--raw", "incr", "ping" ] ================================================ FILE: core/src/test/resources/docker-compose-imagename-overriding-a.yml ================================================ version: "2.1" services: aservice: image: aservice redis: image: redis:a mysql: image: mysql:a custom: build: . networks: custom_network: {} ================================================ FILE: core/src/test/resources/docker-compose-imagename-overriding-b.yml ================================================ version: "2.1" services: redis: image: redis:b mysql: image: mysql:b custom: build: compose-dockerfile networks: custom_network: {} ================================================ FILE: core/src/test/resources/docker-compose-imagename-parsing-dockerfile-with-context.yml ================================================ version: "2.1" services: redis: image: redis mysql: image: mysql custom: build: context: compose-dockerfile dockerfile: Dockerfile networks: custom_network: {} ================================================ FILE: core/src/test/resources/docker-compose-imagename-parsing-dockerfile.yml ================================================ version: "2.1" services: redis: image: redis mysql: image: mysql custom: build: compose-dockerfile networks: custom_network: {} ================================================ FILE: core/src/test/resources/docker-compose-imagename-parsing-v1.yml ================================================ redis: image: redis mysql: image: mysql custom: build: . ================================================ FILE: core/src/test/resources/docker-compose-imagename-parsing-v2-no-version.yml ================================================ services: redis: image: redis mysql: image: mysql custom: build: . networks: custom_network: {} ================================================ FILE: core/src/test/resources/docker-compose-imagename-parsing-v2.yml ================================================ version: "2.1" services: redis: image: redis mysql: image: mysql custom: build: . networks: custom_network: {} ================================================ FILE: core/src/test/resources/docker-compose-non-default-override.yml ================================================ version: '2.1' services: alpine: environment: bar: overwritten ================================================ FILE: core/src/test/resources/dockerfile-build-invalid/.dockerignore ================================================ # This is an invalid dockerignore file *~ [a-b-c] ================================================ FILE: core/src/test/resources/dockerfile-build-invalid/Dockerfile ================================================ FROM alpine:3.16 ================================================ FILE: core/src/test/resources/dockerfile-build-test/.dockerignore ================================================ should_be_ignored.txt ================================================ FILE: core/src/test/resources/dockerfile-build-test/Dockerfile ================================================ FROM alpine:3.16 COPY localfile.txt /test.txt ================================================ FILE: core/src/test/resources/dockerfile-build-test/Dockerfile-alt ================================================ FROM alpine:3.16 RUN echo "test4567" > /test.txt ================================================ FILE: core/src/test/resources/dockerfile-build-test/Dockerfile-buildarg ================================================ FROM alpine:3.16 ARG CUSTOM_ARG RUN echo "${CUSTOM_ARG}" > /test.txt ================================================ FILE: core/src/test/resources/dockerfile-build-test/Dockerfile-currentdir ================================================ FROM alpine:3.16 COPY . / ================================================ FILE: core/src/test/resources/dockerfile-build-test/Dockerfile-from-buildarg ================================================ ARG BUILD_IMAGE ARG BASE_IMAGE ARG BASE_IMAGE_TAG FROM ${BUILD_IMAGE} AS build COPY localfile.txt /test-build.txt FROM $BASE_IMAGE:${BASE_IMAGE_TAG} AS base COPY --from=build /test-build.txt /test.txt ================================================ FILE: core/src/test/resources/dockerfile-build-test/localfile.txt ================================================ test1234 ================================================ FILE: core/src/test/resources/dockerfile-build-test/should_be_ignored.txt ================================================ this file should be ignored by a .dockerignore file ================================================ FILE: core/src/test/resources/dockerfile-build-test/should_not_be_ignored.txt ================================================ this file should not be ignored by a .dockerignore file ================================================ FILE: core/src/test/resources/expectedClasspathFile.txt ================================================ This file exists for org.testcontainers.utility.ClasspathScannerTest ================================================ FILE: core/src/test/resources/fixtures/statements/KeyValuesStatementTest/keyWithNewLinesTest ================================================ "key\nwith\nnewlines"="1" ================================================ FILE: core/src/test/resources/fixtures/statements/KeyValuesStatementTest/keyWithSpacesTest ================================================ "key with spaces"="1" ================================================ FILE: core/src/test/resources/fixtures/statements/KeyValuesStatementTest/keyWithTabsTest ================================================ "key\twith\ttab"="1" ================================================ FILE: core/src/test/resources/fixtures/statements/KeyValuesStatementTest/multilineTest ================================================ "line1"="1" \ "line2"="2" \ "line3"="3" ================================================ FILE: core/src/test/resources/fixtures/statements/KeyValuesStatementTest/valueIsEscapedTest ================================================ "1"="value with spaces" \ "2"="value\nwith\nnewlines" \ "3"="value\twith\ttab" ================================================ FILE: core/src/test/resources/fixtures/statements/MultiArgsStatementTest/multilineTest ================================================ ["some\nmultiline\nargument"] ================================================ FILE: core/src/test/resources/fixtures/statements/MultiArgsStatementTest/simpleTest ================================================ ["a","b","c"] ================================================ FILE: core/src/test/resources/fixtures/statements/RawStatementTest/simpleTest ================================================ value as \ is ================================================ FILE: core/src/test/resources/fixtures/statements/SingleArgumentStatementTest/multilineTest ================================================ hello\ world ================================================ FILE: core/src/test/resources/fixtures/statements/SingleArgumentStatementTest/simpleTest ================================================ hello ================================================ FILE: core/src/test/resources/health-wait-strategy-dockerfile/Dockerfile ================================================ FROM alpine:3.16 HEALTHCHECK --interval=1s CMD test -e /testfile COPY write_file_and_loop.sh write_file_and_loop.sh RUN chmod +x write_file_and_loop.sh CMD ["/write_file_and_loop.sh"] ================================================ FILE: core/src/test/resources/health-wait-strategy-dockerfile/write_file_and_loop.sh ================================================ #!/bin/ash set -ex sleep 2 touch /testfile while true; do sleep 1; done ================================================ FILE: core/src/test/resources/https-wait-strategy-dockerfile/Dockerfile ================================================ FROM nginx:1.17-alpine # Create keypair and self-signed certificate for https test RUN apk update && apk add bash openssl && openssl req -batch -x509 -nodes -days 365 -newkey rsa:2048 -subj "/CN=localhost" -keyout /etc/ssl/private/nginx-selfsigned.key -out /etc/ssl/certs/nginx-selfsigned.crt ADD nginx-ssl.conf /etc/nginx/conf.d/default.conf ================================================ FILE: core/src/test/resources/https-wait-strategy-dockerfile/nginx-ssl.conf ================================================ # This configuration makes Nginx listen on port port 8443 # In order to use this config, add this line to the Dockerfile to create the keypair and self-signed certificate: # RUN apk update && apk add openssl && openssl req -batch -x509 -nodes -days 365 -newkey rsa:2048 -subj "/CN=localhost" -keyout /etc/ssl/private/nginx-selfsigned.key -out /etc/ssl/certs/nginx-selfsigned.crt server { listen 8443 ssl; server_name localhost; ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt; ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key; } ================================================ FILE: core/src/test/resources/internal-port-check-dockerfile/Dockerfile-bash ================================================ FROM nginx:1.17-alpine RUN apk add bash # Make sure the /proc/net/tcp* check fails in this container RUN rm /usr/bin/awk # Make sure the nc check fails in this container RUN rm /usr/bin/nc ADD nginx.conf /etc/nginx/conf.d/default.conf ================================================ FILE: core/src/test/resources/internal-port-check-dockerfile/Dockerfile-nc ================================================ FROM nginx:1.17-alpine # If this fails, you ended up using a base image with bash installed. Consider removing /bin/bash in this case RUN if bash -c true &> /dev/null; then exit 1; fi # Make sure the /proc/net/tcp* check fails in this container RUN rm /usr/bin/awk ADD nginx.conf /etc/nginx/conf.d/default.conf ================================================ FILE: core/src/test/resources/internal-port-check-dockerfile/Dockerfile-tcp ================================================ FROM nginx:1.17-alpine # If this fails, you ended up using a base image with bash installed. Consider removing /bin/bash in this case RUN if bash -c true &> /dev/null; then exit 1; fi # Make sure the nc check fails in this container RUN rm /usr/bin/nc ADD nginx.conf /etc/nginx/conf.d/default.conf ================================================ FILE: core/src/test/resources/internal-port-check-dockerfile/nginx.conf ================================================ # This configuration makes Nginx listen on port port 8080 server { # Port 8080 is necessary to prove that the command formatting in the /proc/net/tcp* check uses the correct casing for hexadecimal numbers (i.e. 1F90 and not 1f90) listen 8080; # Port 100 is necessary to ensure that the /proc/net/tcp* check also succeeds with trailing zeros in the hexadecimal port listen 100; } ================================================ FILE: core/src/test/resources/invalid-compose.yml ================================================ this is not a valid docker-compose file ================================================ FILE: core/src/test/resources/local-compose-test.yml ================================================ version: '2.1' services: redis: image: redis-local:latest ports: - 6379 ================================================ FILE: core/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: core/src/test/resources/mappable-dockerfile/Dockerfile ================================================ FROM alpine:3.16 ADD folder/someFile.txt /someFile.txt RUN cat /someFile.txt ADD test.txt /test.txt RUN cat /test.txt CMD ping -c 5 www.google.com ================================================ FILE: core/src/test/resources/mappable-resource/test-resource.txt ================================================ FOOBAR ================================================ FILE: core/src/test/resources/redis.conf ================================================ ================================================ FILE: core/src/test/resources/scaled-compose-test.yml ================================================ redis: image: redis ================================================ FILE: core/src/test/resources/test-recursive-file.txt ================================================ Used for DirectoryTarResourceTest ================================================ FILE: core/src/test/resources/test_copy_to_container.txt ================================================ Some Test Message ================================================ FILE: core/src/test/resources/v2-compose-test-passthrough.yml ================================================ version: '2.1' services: alpine: build: compose-dockerfile environment: bar: ${foo} ================================================ FILE: core/src/test/resources/v2-compose-test-port-via-env.yml ================================================ services: redis: image: redis ports: - ${REDIS_PORT} ================================================ FILE: core/src/test/resources/v2-compose-test-with-network.yml ================================================ version: '2.1' services: redis: image: redis networks: - redis-net networks: redis-net: ================================================ FILE: core/src/test/resources/v2-compose-test.yml ================================================ version: '2.1' services: redis: image: redis ports: - 6379 ================================================ FILE: core/testlib/META-INF/dummy_unique_name.txt ================================================ This is a file inside a JAR archive ================================================ FILE: core/testlib/README.md ================================================ This directory contains a synthetic JAR (`fakejar.jar`) that is needed for `org.testcontainers.utility.MountableFileTest`. The `create_fakejar.sh` script may be used to recreate it. ================================================ FILE: core/testlib/create_fakejar.sh ================================================ #!/usr/bin/env bash rm fakejar.jar zip -r fakejar.jar META-INF/ recursive/ mv fakejar.jar repo/fakejar/fakejar/0/fakejar-0.jar ================================================ FILE: core/testlib/recursive/dir/content.txt ================================================ This is example content to show recursive mounting of files from inside a JAR archive. ================================================ FILE: core/testlib/repo/fakejar/fakejar/0/fakejar-0.pom ================================================ 4.0.0 fakejar fakejar 0 POM was created from install:install-file ================================================ FILE: core/testlib/repo/fakejar/fakejar/maven-metadata-local.xml ================================================ fakejar fakejar 0 0 20170414082010 ================================================ FILE: docker-compose.yml ================================================ version: "3.7" services: docs: image: python:3.8 command: sh -c "pip install -r requirements.txt && mkdocs serve -a 0.0.0.0:8000" working_dir: /docs volumes: - ./:/docs ports: - 8000:8000 ================================================ FILE: docs/_headers ================================================ /search/search_index.json Access-Control-Allow-Origin: * ================================================ FILE: docs/_redirects ================================================ # Each redirect rule must be listed on a separate line, with the original path followed by the new path or URL. /usage/docker_compose.html /modules/docker_compose/ /usage/generic_containers.html /features/creating_container/ /usage/windows_support.html /supported_docker_environment/windows/ /usage/dockerfile.html /features/creating_images/ /usage/inside_docker.html /supported_docker_environment/continuous_integration/dind_patterns/ /usage/webdriver_containers.html /modules/webdriver_containers/ /usage/properties.html /features/configuration/ /usage/kafka_containers.html /modules/kafka/ /usage/elasticsearch_container.html /modules/elasticsearch/ /usage/database_containers.html /modules/databases/ /usage/neo4j_container.html /modules/databases/neo4j/ /compatibility.html /supported_docker_environment/ /on_failure.html /error_missing_container_runtime_environment # No great 1:1 mapping exists for the following, so redirect to somewhere where at least a sensible sidebar will be shown /usage/options.html /features/creating_container/ /ci/ci.html /supported_docker_environment/ /usage.html / ================================================ FILE: docs/bounty.md ================================================ # Testcontainers issue bounty policy ## General We want to use issue bounties to encourage contributions in areas that are important to our sponsors, or tricky to solve. This includes bug fixes and new features. We hope that this will provide incentives to tackle issues, and gives sponsors a way to influence where development time is expended. We also want to reward our contributors, some of whom make huge efforts to improve Testcontainers and help their fellow developers! !!! note It's early days for our use of sponsorship, so we expect to evolve this policy over time, possibly without notice. In the event of any ambiguity or dispute, the [Testcontainers org core maintainers](#organisation-core-maintainers) have the final say. If you'd like to suggest an improvement to this policy, we'd be grateful for your input - please raise a pull request! ## For Sponsors Sponsors will be able to create a number of 'bounties' per month, varying according to sponsorship tier. As a sponsor, the process for creating a bounty is as follows: * Raise an issue, or find an existing issue that describes the bug or feature. * Start a discussion with the [Testcontainers org core maintainers](#organisation-core-maintainers) to agree that the issue is suitable for a bounty, and how much the reward amount should be. * Once agreed, we will assign a label to the issue so that interested developers can find it. Sponsors can create up to 1 or 3 bounties (according to tier) _per calendar month_ - i.e. the counter resets on the 1st of each month. If a sponsor does not use their full quota of bounty credits in a calendar month, they cannot be rolled over to the next month. Bounties will expire 90 days after creation - after this time, if they have not been resolved we will close them. ## For Contributors As a contributor, the process for working on an issue with a bounty attached is: * Find an issue with a bounty attached to it and no assignee, clarify the requirements if necessary, and consider how you would approach working on it. * Start a discussion with the [Testcontainers org core maintainers](#organisation-core-maintainers) and the bounty owner. To avoid unpleasant surprises at review time, we'll try to confirm that we're happy with your proposed solution. * If we're happy with your proposed solution, we will assign the ticket to you. * Once work is complete, we will go through the PR process as usual and merge the work when finished. * To receive the bounty reward, [raise an invoice](https://opencollective.com/testcontainers/expenses/new) on Open Collective, following the expenses policy on that page. Note that a 20% cut of the bounty amount will normally be assigned to project maintainers for PR review work. We believe this reflects that PR review can often be a significant amount of work for some issues - and also gives maintainers an incentive to complete the review and unlock the bounty reward! Some pull requests are so well done that very little review is necessary. If that happens, the maintainers may choose not to take a cut of the bounty, and instead release the full amount to the contributor. ## Organisation core maintainers The organisation core maintainers are: * Richard North (@rnorth) * Sergei Egorov (@bsideup) * Kevin Wittek (@kiview) ================================================ FILE: docs/contributing.md ================================================ # Contributing * Star the project on [GitHub](https://github.com/testcontainers/testcontainers-java) and help spread the word :) * Join our [Slack workspace](http://slack.testcontainers.org) * [Start a discussion](https://github.com/testcontainers/testcontainers-java/discussions) if you have an idea, find a possible bug or have a general question. * Contribute improvements or fixes using a [Pull Request](https://github.com/testcontainers/testcontainers-java/pulls). If you're going to contribute, thank you! Please just be sure to: * discuss with the authors prior to doing anything big. * follow the style, naming and structure conventions of the rest of the project. * make commits atomic and easy to merge. * when updating documentation, please see [our guidance for documentation contributions](contributing_docs.md). * apply format running `./gradlew spotlessApply` (this requires [Node.js](https://nodejs.org/) to be installed on your machine, one of the [package managers](https://nodejs.org/en/download/package-manager/) might be handy) * verify all tests are passing. Build the project with `./gradlew check` to do this. **N.B.** Gradle's Build Cache is enabled by default, but you can add `--no-build-cache` flag to disable it. ## Contributing new modules We often receive proposals (or fully formed PRs) for new modules. We're very happy to have contributions, but new modules require specific extra care. We want to balance: * Usefulness of the module. * Our ability to support the module in the future, potentially after contributors have moved on. * Contributors time, so that nobody puts in wasted effort. ### Does it need to be a module? *N.B. this is not a perfect list - please always reach out to us before starting on a module contribution!* * Does the module enable use of Testcontainers with a popular or rapidly growing technology? * Does the module 'add value' beyond a `GenericContainer` code snippet/example? e.g. * does it neatly encapsulate a difficult problem of running the program in a container? * does it add technology-specific [wait strategies](features/startup_and_waits.md)? * does it enable straightforward usage of client libraries? If the answers to the above are all yes, then a new module may be a good approach. Otherwise, it is entirely possible for you to: * publish a code snippet * contribute an example to the Testcontainers repo * publish your own third party library In any case, please contact us to help validate your proposal! ### Checklist *Suggestion: copy and paste this list into PRs for new modules.* Every item on this list will require judgement by the Testcontainers core maintainers. Exceptions will sometimes be possible; items with `should` are more likely to be negotiable than those items with `must`. #### Default docker image - [ ] Should be a Docker Hub official image, or published by a reputable source (ideally the company or organisation that officially supports the technology) - [ ] Should have a verifiable open source Dockerfile and a way to view the history of changes - [ ] MUST show general good practices regarding container image tagging - e.g. we do not use `latest` tags, and we do not use tags that may be mutated in the future - [ ] MUST be legal for Testcontainers developers and Testcontainers users to pull and use. Mechanisms exist to allow EULA acceptance to be signalled, but images that can be used without a licence are greatly preferred. #### Module dependencies - [ ] The module should use as few dependencies as possible, - [ ] Regarding libraries, either: - they should be `compileOnly` if they are likely to already be on the classpath for users' tests (e.g. client libraries or drivers) - they can be `implementation` (and thus transitive dependencies) if they are very unlikely to conflict with users' dependencies. - [ ] If client libraries are used to test or use the module, these MUST be legal for Testcontainers developers and Testcontainers users to download and use. #### API (e.g. `MyModuleContainer` class) - [ ] Favour doing the right thing, and least surprising thing, by default - [ ] Ensure that method and parameter names are easy to understand. Many users will ignore documentation, so IDE-based substitutes (autocompletion and Javadocs) should be intuitive. - [ ] The module's public API should only handle standard JDK data types and MUST not expose data types that come from `compileOnly` dependencies. This is to reduce the risk of compatibility problems with future versions of third party libraries. #### Documentation - [ ] Every module MUST have a dedicated documentation page containing: - [ ] A high level overview - [ ] A usage example - [ ] If appropriate, basic API documentation or further usage guidelines - [ ] Dependency information - [ ] Acknowledgements, if appropriate - [ ] Consider that many users will not read the documentation pages - even if the first person to add it to a project does, people reading/updating the code in the future may not. Try and avoid the need for critical knowledge that is only present in documentation. ### Incubating modules We have a policy of marking new modules as 'incubating' so that we can evaluate its maintainability and usability traits over a longer period of time. We currently believe 3 months is a fair period of time, but may change this. New modules should have the following warning at the top of their documentation pages: !!! note This module is INCUBATING. While it is ready for use and operational in the current version of Testcontainers, it is possible that it may receive breaking changes in the future. See [our contributing guidelines](/contributing/#incubating-modules) for more information on our incubating modules policy. We will evaluate incubating modules periodically, and remove the label when appropriate. ## Combining Dependabot PRs Since we generally get a lot of Dependabot PRs, we regularly combine them into single commits. For this, we are using the [gh-combine-prs](https://github.com/rnorth/gh-combine-prs) extension for [GitHub CLI](https://cli.github.com/). The whole process is as follows: 1. Check that all open Dependabot PRs did succeed their build. If they did not succeed, trigger a rerun if the cause were external factors or else document the reason if obvious. 2. Run the extension from an up-to-date local `main` branch: `gh combine-prs --query "author:app/dependabot"` 3. Merge conflicts might appear. Just ignore them, we will get those PRs in a future run. 4. Once the build of the combined PR did succeed, temporarily enable merge commits and merge the PR using a merge commit through the GitHub UI. 5. After the merge, disable merge commits again. ================================================ FILE: docs/contributing_docs.md ================================================ # Contributing to documentation The Testcontainers for Java documentation is a static site built with [MkDocs](https://www.mkdocs.org/). We use the [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) theme, which offers a number of useful extensions to MkDocs. In addition we use a [custom plugin](https://github.com/rnorth/mkdocs-codeinclude-plugin) for inclusion of code snippets. We publish our documentation using Netlify. ## Previewing rendered content ### Using Docker locally The root of the project contains a `docker-compose.yml` file. Simply run `docker-compose up` and then access the docs at [http://localhost:8000](http://localhost:8000). ### Using Python locally * Ensure that you have Python 3.8.0 or higher. * Set up a virtualenv and run `pip install -r requirements.txt` in the `testcontainers-java` root directory. * Once Python dependencies have been installed, run `mkdocs serve` to start a local auto-updating MkDocs server. ### PR Preview deployments Note that documentation for pull requests will automatically be published by Netlify as 'deploy previews'. These deployment previews can be accessed via the `deploy/netlify` check that appears for each pull request. ## Codeincludes The Gradle project under `docs/examples` is intended to hold compilable, runnable example code that can be included as snippets into the documentation at build-time. As a result, we can have more confidence that code samples shown in the documentation is valid. We use a custom plugin for MkDocs to include snippets into our docs. A codeinclude block will resemble a regular markdown link surrounded by a pair of XML comments, e.g.:

<!--codeinclude-->
[Human readable title for snippet](./relative_path_to_example_code.java) targeting_expression
<!--/codeinclude-->
Where `targeting_expression` could be: * `block:someString` or * `inside_block:someString` If these are provided, the macro will seek out any line containing the token `someString` and grab the next curly brace delimited block that it finds. `block` will grab the starting line and closing brace, whereas `inside_block` will omit these. e.g., given: ```java public class FooService { public void doFoo() { foo.doSomething(); } ... ``` If we use `block:doFoo` as our targeting expression, we will have the following content included into our page: ```java public void doFoo() { foo.doSomething(); } ``` Whereas using `inside_block:doFoo` we would just have the inner content of the method included: ```java foo.doSomething(); ``` Note that: * Any code included will have its indentation reduced * Every line in the source file will be searched for an instance of the token (e.g. `doFoo`). If more than one line includes that token, then potentially more than one block could be targeted for inclusion. It is advisable to use a specific, unique token to avoid unexpected behaviour. When we wish to include a section of code that does not naturally appear within braces, we can simply insert our token, with matching braces, in a comment. While a little ugly, this has the benefit of working in any context and is easy to understand. For example: ```java public class FooService { public void boringMethod() { doSomethingBoring(); // doFoo { doTheThingThatWeActuallyWantToShow(); // } } ``` ================================================ FILE: docs/css/extra.css ================================================ h1, h2, h3, h4, h5, h6 { font-family: 'Rubik', sans-serif; } [data-md-color-scheme="testcontainers"] { --md-primary-fg-color: #00bac2; --md-accent-fg-color: #361E5B; --md-typeset-a-color: #0C94AA; --md-primary-fg-color--dark: #291A3F; --md-default-fg-color--lightest: #F2F4FE; --md-footer-fg-color: #361E5B; --md-footer-fg-color--light: #746C8F; --md-footer-fg-color--lighter: #C3BEDE; --md-footer-bg-color: #F7F9FD; --md-footer-bg-color--dark: #F7F9FD; } .card-grid { display: grid; gap: 10px; } .tc-version { font-size: 1.1em; text-align: center; margin: 0; } @media (min-width: 680px) { .card-grid { grid-template-columns: repeat(3, 1fr); } } body .card-grid-item { display: flex; align-items: center; gap: 20px; border: 1px solid #C3BEDE; border-radius: 6px; padding: 16px; font-weight: 600; color: #9991B5; background: #F2F4FE; } body .card-grid-item:hover, body .card-grid-item:focus { color: #9991B5; } .card-grid-item[href] { color: var(--md-primary-fg-color--dark); background: transparent; } .card-grid-item[href]:hover, .card-grid-item[href]:focus { background: #F2F4FE; color: var(--md-primary-fg-color--dark); } .community-callout-wrapper { padding: 30px 10px 0 10px; } .community-callout { color: #F2F4FE; background: linear-gradient(10.88deg, rgba(102, 56, 242, 0.4) 9.56%, #6638F2 100%), #291A3F; box-shadow: 0px 20px 45px rgba(#9991B5, 0.75); border-radius: 10px; padding: 20px; } .community-callout h2 { font-size: 1.15em; margin: 0 0 20px 0; color: #F2F4FE; text-align: center; } .community-callout ul { list-style: none; padding: 0; display: flex; justify-content: space-between; gap: 10px; margin-top: 20px; margin-bottom: 0; } .community-callout a { transition: opacity 0.2s ease; } .community-callout a:hover { opacity: 0.5; } .community-callout a img { height: 1.75em; width: auto; aspect-ratio: 1; } @media (min-width: 1220px) { .community-callout-wrapper { padding: 40px 0 0; } .community-callout h2 { font-size: 1.25em; } .community-callout a img { height: 2em; } } @media (min-width: 1600px) { .community-callout h2 { font-size: 1.15em; } .community-callout a img { height: 1.75em; } } ================================================ FILE: docs/css/tc-header.css ================================================ :root { --color-catskill: #F2F4FE; --color-catskill-45: rgba(242, 244, 254, 0.45); --color-mist: #E7EAFB; --color-fog: #C3C7E6; --color-smoke: #9991B5; --color-smoke-75: rgba(153, 145, 181, 0.75); --color-storm: #746C8F; --color-topaz: #00BAC2; --color-pacific: #17A6B2; --color-teal: #027F9E; --color-eggplant: #291A3F; --color-plum: #361E5B; } #site-header { color: var(--color-storm); background: #fff; font-family: 'Rubik', Arial, Helvetica, sans-serif; font-size: 12px; line-height: 1.5; position: relative; width: 100%; z-index: 4; display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 20px; } body.tc-header-active #site-header { z-index: 5; } #site-header .brand { display: flex; justify-content: space-between; gap: 20px; width: 100%; } #site-header .logo { display: flex; } #site-header .logo img, #site-header .logo svg { height: 30px; width: auto; max-width: 100%; } #site-header #mobile-menu-toggle { background: none; border: none; display: flex; align-items: center; gap: 10px; cursor: pointer; color: var(--color-eggplant); padding: 0; margin: 0; font-weight: 500; } body.mobile-menu #site-header #mobile-menu-toggle { color: var(--color-topaz); } #site-header ul { list-style: none; padding: 0; margin: 0; } #site-header nav { display: none; } #site-header .menu-item { display: flex; } #site-header .menu-item button, #site-header .menu-item a { min-height: 30px; display: flex; gap: 6px; align-items: center; border: none; background: none; cursor: pointer; padding: 0; font-weight: 500; color: var(--color-eggplant); text-decoration: none; font-size: 14px; transition: color 0.2s ease; white-space: nowrap; } #site-header .menu-item .badge { color: white; font-size: 10px; padding: 2px 6px; background-color: #0FD5C6; // somehow $topaz is too dark for me. text-align: center; text-decoration: none; display: inline-block; border-radius: 6px; &:hover { } } #site-header .menu-item button:hover, #site-header .menu-item a:hover { color: var(--color-topaz); } #site-header .menu-item button .icon-external, #site-header .menu-item a .icon-externa { margin-left: auto; opacity: .3; flex-shrink: 0; } #site-header .menu-item button .icon-caret, #site-header .menu-item a .icon-caret { opacity: .3; height: 8px; } #site-header .menu-item button .icon-slack, #site-header .menu-item a .icon-slack, #site-header .menu-item button .icon-github, #site-header .menu-item a .icon-github { height: 18px; } #site-header .menu-item .menu-dropdown { flex-direction: column; } body #site-header .menu-item .menu-dropdown { display: none; } #site-header .menu-item.has-children.active .menu-dropdown { display: flex; z-index: 10; } #site-header .menu-dropdown-item + .menu-dropdown-item { border-top: 1px solid var(--color-mist); } #site-header .menu-dropdown-item a { display: flex; gap: 10px; align-items: center; padding: 10px 20px; font-weight: 500; color: var(--color-eggplant); text-decoration: none; transition: color 0.2s ease, background 0.2s ease; } #site-header .menu-dropdown-item a .icon-external { margin-left: auto; color: var(--color-fog); flex-shrink: 0; opacity: 1; } #site-header .menu-dropdown-item a:hover { background-color: var(--color-catskill-45); } #site-header .menu-dropdown-item a:hover .icon-external { color: var(--color-topaz); } #site-header .menu-dropdown-item a img { height: 24px; } .md-header { background-color: var(--color-catskill); color: var(--color-eggplant); } .md-header.md-header--shadow { box-shadow: none; } .md-header__inner.md-grid { max-width: 100%; padding: 1.5px 20px; } [dir=ltr] .md-header__title { margin: 0; } .md-header__topic:first-child { font-size: 16px; font-weight: 500; font-family: 'Rubik', Arial, Helvetica, sans-serif; } .md-header__title.md-header__title--active .md-header__topic, .md-header__title[data-md-state=active] .md-header__topic { opacity: 1; pointer-events: all; transform: translateX(0); transition: none; z-index: 0; } .md-header__topic a { max-width: 100%; overflow: hidden; text-overflow: ellipsis; transition: color .2s ease; } .md-header__topic a:hover { color: var(--color-topaz); } div.md-header__source { width: auto; } div.md-source__repository { max-width: 100%; } .md-main { padding: 0 12px; } @media screen and (min-width: 60em) { form.md-search__form { background-color: #FBFBFF; color: var(--color-storm); } form.md-search__form:hover { background-color: #fff; } .md-search__input + .md-search__icon { color: var(--color-plum); } .md-search__input::placeholder { color: var(--color-smoke); } } @media (min-width: 500px) { #site-header { font-size: 16px; padding: 20px 40px; } #site-header .logo img, #site-header .logo svg { height: 48px; } #site-header .menu-item button .icon-caret, #site-header .menu-item a .icon-caret { height: 10px; } #site-header .menu-item button .icon-slack, #site-header .menu-item a .icon-slack, #site-header .menu-item button .icon-github, #site-header .menu-item a .icon-github { height: 24px; } .md-header__inner.md-grid { padding: 5px 40px; } .md-main { padding: 0 32px; } } @media (min-width: 1024px) { #site-header #mobile-menu-toggle { display: none; } #site-header nav { display: block; } #site-header .menu { display: flex; justify-content: center; gap: 30px; } #site-header .menu-item { align-items: center; position: relative; } #site-header .menu-item button, #site-header .menu-item a { min-height: 48px; gap: 8px; font-size: 16px; } #site-header .menu-item .menu-dropdown { position: absolute; top: 100%; right: -8px; border: 1px solid var(--color-mist); border-radius: 6px; background: #fff; box-shadow: 0px 30px 35px var(--color-smoke-75); min-width: 200px; } } @media (max-width: 1023px) { #site-header { flex-direction: column; } body.mobile-tc-header-active #site-header { z-index: 5; } body.mobile-menu #site-header nav { display: flex; } #site-header nav { position: absolute; top: calc(100% - 5px); width: calc(100% - 80px); flex-direction: column; border: 1px solid var(--color-mist); border-radius: 6px; background: #fff; box-shadow: 0px 30px 35px var(--color-smoke-75); min-width: 200px; } #site-header .menu-item { flex-direction: column; } #site-header .menu-item + .menu-item { border-top: 1px solid var(--color-mist); } #site-header .menu-item button, #site-header .menu-item a { padding: 10px 20px; } #site-header .menu-item.has-children.active .menu-dropdown { border-top: 1px solid var(--color-mist); } #site-header .menu-dropdown-item a { padding: 10px 20px 10px 30px; } } @media (max-width: 499px) { #site-header nav { width: calc(100% - 40px); } } ================================================ FILE: docs/error_missing_container_runtime_environment.md ================================================ # Fixing Issues with Discovering A Supported Container Runtime Environment If you ended up on this page, it seems that either Testcontainers was not able to find a supported container runtime in your environment, or you found this page while searching for information to deal with errors regarding the environment discovery mechanism of Testcontainers. Testcontainers requires a supported container runtime environment to be present in order to manage and run containers. Here is a list of supported container runtime environments: * [Docker Desktop](https://www.docker.com/products/docker-desktop/) * [Docker Engine on Linux](https://docs.docker.com/engine/install/) * [Testcontainers Cloud](https://www.testcontainers.cloud?utm_medium=direct&utm_source=testcontainers.com&utm_content=docs&utm_term=on-failure) For more extensive information on supported container runtime environments, as well as known limitations of alternative container runtime environments, please refer to [this page](https://java.testcontainers.org/supported_docker_environment/) in our documentation. ================================================ FILE: docs/examples/junit4/generic/build.gradle ================================================ description = "Examples for docs" dependencies { testImplementation "junit:junit:4.13.2" testImplementation project(":testcontainers") testImplementation project(":testcontainers-selenium") testImplementation project(":testcontainers-mysql") testRuntimeOnly 'com.mysql:mysql-connector-j:8.2.0' testImplementation "org.seleniumhq.selenium:selenium-api:4.35.0" testImplementation 'org.assertj:assertj-core:3.27.4' } ================================================ FILE: docs/examples/junit4/generic/src/test/java/generic/CmdModifierTest.java ================================================ package generic; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.model.Info; import org.junit.Rule; import org.junit.Test; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.Container; import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.DockerImageName; import java.io.IOException; import java.util.Objects; import static org.assertj.core.api.Assertions.assertThat; public class CmdModifierTest { // hostname { @Rule public GenericContainer theCache = new GenericContainer<>(DockerImageName.parse("redis:6-alpine")) .withCreateContainerCmdModifier(cmd -> cmd.withHostName("the-cache")); // } // spotless:off // memory { private long memoryInBytes = 32l * 1024l * 1024l; private long memorySwapInBytes = 64l * 1024l * 1024l; @Rule public GenericContainer memoryLimitedRedis = new GenericContainer<>(DockerImageName.parse("redis:6-alpine")) .withCreateContainerCmdModifier(cmd -> { cmd.getHostConfig() .withMemory(memoryInBytes) .withMemorySwap(memorySwapInBytes); }); // } // spotless:on @Test public void testHostnameModified() throws IOException, InterruptedException { final Container.ExecResult execResult = theCache.execInContainer("hostname"); assertThat(execResult.getStdout().trim()).isEqualTo("the-cache"); } @Test public void testMemoryLimitModified() throws IOException, InterruptedException { final Container.ExecResult execResult = memoryLimitedRedis.execInContainer("cat", getMemoryLimitFilePath()); assertThat(execResult.getStdout().trim()).isEqualTo(String.valueOf(memoryInBytes)); } private String getMemoryLimitFilePath() { DockerClient dockerClient = DockerClientFactory.instance().client(); Info info = dockerClient.infoCmd().exec(); Object cgroupVersion = info.getRawValues().get("CgroupVersion"); boolean cgroup2 = Objects.equals("2", cgroupVersion); if (cgroup2) { return "/sys/fs/cgroup/memory.max"; } return "/sys/fs/cgroup/memory/memory.limit_in_bytes"; } } ================================================ FILE: docs/examples/junit4/generic/src/test/java/generic/CommandsTest.java ================================================ package generic; import org.junit.Rule; import org.junit.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.DockerImageName; import static org.assertj.core.api.Assertions.assertThat; public class CommandsTest { @Rule // startupCommand { public GenericContainer redisWithCustomPort = new GenericContainer(DockerImageName.parse("redis:6-alpine")) .withCommand("redis-server --port 7777") // } .withExposedPorts(7777); @Test public void testStartupCommandOverrideApplied() { assertThat(redisWithCustomPort.isRunning()).isTrue(); // good enough to check that the container started listening } } ================================================ FILE: docs/examples/junit4/generic/src/test/java/generic/ContainerCreationTest.java ================================================ package generic; import org.junit.ClassRule; import org.junit.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.DockerImageName; import static org.assertj.core.api.Assertions.assertThat; public class ContainerCreationTest { // spotless:off // simple { public static final DockerImageName REDIS_IMAGE = DockerImageName.parse("redis:6-alpine"); @ClassRule public static GenericContainer redis = new GenericContainer<>(REDIS_IMAGE) .withExposedPorts(6379); // } // spotless:on public static final DockerImageName ALPINE_IMAGE = DockerImageName.parse("alpine:3.17"); // spotless:off // withOptions { // Set up a plain OS container and customize environment, // command and exposed ports. This just listens on port 80 // and always returns '42' @ClassRule public static GenericContainer alpine = new GenericContainer<>(ALPINE_IMAGE) .withExposedPorts(80) .withEnv("MAGIC_NUMBER", "42") .withCommand("/bin/sh", "-c", "while true; do echo \"$MAGIC_NUMBER\" | nc -l -p 80; done"); // } // spotless:on @Test public void testStartup() { assertThat(redis.isRunning()).isTrue(); // good enough to check that the container started listening assertThat(alpine.isRunning()).isTrue(); // good enough to check that the container started listening } } ================================================ FILE: docs/examples/junit4/generic/src/test/java/generic/ContainerLabelTest.java ================================================ package generic; import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.DockerImageName; import java.util.HashMap; import java.util.Map; public class ContainerLabelTest { // single_label { public GenericContainer containerWithLabel = new GenericContainer(DockerImageName.parse("alpine:3.17")) .withLabel("key", "value"); // } // multiple_labels { private Map mapOfLabels = new HashMap<>(); // populate map, e.g. mapOfLabels.put("key1", "value1"); public GenericContainer containerWithMultipleLabels = new GenericContainer(DockerImageName.parse("alpine:3.17")) .withLabels(mapOfLabels); // } } ================================================ FILE: docs/examples/junit4/generic/src/test/java/generic/DependsOnTest.java ================================================ package generic; import org.junit.Rule; import org.junit.Test; import org.testcontainers.containers.GenericContainer; import static org.assertj.core.api.Assertions.assertThat; public class DependsOnTest { @Rule // dependsOn { public GenericContainer redis = new GenericContainer<>("redis:6-alpine").withExposedPorts(6379); @Rule public GenericContainer nginx = new GenericContainer<>("nginx:1.27.0-alpine3.19-slim") .dependsOn(redis) .withExposedPorts(80); // } @Test public void testContainersAllStarted() { assertThat(redis.isRunning()).isTrue(); assertThat(nginx.isRunning()).isTrue(); } } ================================================ FILE: docs/examples/junit4/generic/src/test/java/generic/ExampleImageNameSubstitutor.java ================================================ package generic; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.ImageNameSubstitutor; public class ExampleImageNameSubstitutor extends ImageNameSubstitutor { @Override public DockerImageName apply(DockerImageName original) { // convert the original name to something appropriate for // our build environment return DockerImageName.parse( // your code goes here - silly example of capitalising // the original name is shown original.asCanonicalNameString().toUpperCase() ); } @Override protected String getDescription() { // used in logs return "example image name substitutor"; } } ================================================ FILE: docs/examples/junit4/generic/src/test/java/generic/ExecTest.java ================================================ package generic; import org.junit.Rule; import org.junit.Test; import org.testcontainers.containers.Container; import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.DockerImageName; import java.io.IOException; import static org.assertj.core.api.Assertions.assertThat; public class ExecTest { @Rule public GenericContainer container = new GenericContainer<>(DockerImageName.parse("alpine:3.17")) .withCommand("top"); @Test public void testSimpleExec() throws IOException, InterruptedException { // standaloneExec { container.execInContainer("touch", "/somefile.txt"); // } // execReadingStdout { Container.ExecResult lsResult = container.execInContainer("ls", "-al", "/"); String stdout = lsResult.getStdout(); int exitCode = lsResult.getExitCode(); assertThat(stdout).contains("somefile.txt"); assertThat(exitCode).isZero(); // } } } ================================================ FILE: docs/examples/junit4/generic/src/test/java/generic/HostPortExposedTest.java ================================================ package generic; import com.sun.net.httpserver.HttpServer; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.openqa.selenium.chrome.ChromeOptions; import org.openqa.selenium.remote.RemoteWebDriver; import org.testcontainers.Testcontainers; import org.testcontainers.containers.BrowserWebDriverContainer; import java.io.OutputStream; import java.net.InetSocketAddress; import static org.assertj.core.api.Assertions.assertThat; public class HostPortExposedTest { private static HttpServer server; private static int localServerPort; @BeforeClass public static void setUp() throws Exception { server = HttpServer.create(new InetSocketAddress(0), 0); server.createContext( "/", exchange -> { byte[] content = "Hello World!".getBytes(); exchange.sendResponseHeaders(200, content.length); try (OutputStream responseBody = exchange.getResponseBody()) { responseBody.write(content); responseBody.flush(); } } ); server.start(); localServerPort = server.getAddress().getPort(); // exposePort { Testcontainers.exposeHostPorts(localServerPort); // } } @AfterClass public static void tearDown() throws Exception { server.stop(0); } @Rule public BrowserWebDriverContainer browser = new BrowserWebDriverContainer<>() .withCapabilities(new ChromeOptions()); @Test public void testContainerRunningAgainstExposedHostPort() { // useHostExposedPort { final String rootUrl = String.format("http://host.testcontainers.internal:%d/", localServerPort); final RemoteWebDriver webDriver = new RemoteWebDriver(this.browser.getSeleniumAddress(), new ChromeOptions()); webDriver.get(rootUrl); // } final String pageSource = webDriver.getPageSource(); assertThat(pageSource).contains("Hello World!"); } } ================================================ FILE: docs/examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java ================================================ package generic; import generic.support.TestSpecificImageNameSubstitutor; import org.junit.Test; import org.testcontainers.containers.MySQLContainer; import org.testcontainers.utility.DockerImageName; public class ImageNameSubstitutionTest { @Test public void simpleExample() { try ( // spotless:off // directDockerHubReference { // Referring directly to an image on Docker Hub (mysql:8.0.36) final MySQLContainer mysql = new MySQLContainer<>( DockerImageName.parse("mysql:8.0.36") ) // start the container and use it for testing // } // spotless:on ) { mysql.start(); } } /** * Note that this test uses a fake image name, which will only work because * {@link TestSpecificImageNameSubstitutor} steps in to override the substitution for this exact * image name. */ @Test public void substitutedExample() { try ( // spotless:off // hardcodedMirror { // Referring directly to an image on a private registry - image name will vary final MySQLContainer mysql = new MySQLContainer<>( DockerImageName.parse("registry.mycompany.com/mirror/mysql:8.0.36") .asCompatibleSubstituteFor("mysql") ) // start the container and use it for testing // } // spotless:on ) { mysql.start(); } } } ================================================ FILE: docs/examples/junit4/generic/src/test/java/generic/MultiplePortsExposedTest.java ================================================ package generic; import org.junit.Rule; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.output.Slf4jLogConsumer; import org.testcontainers.utility.DockerImageName; public class MultiplePortsExposedTest { private static final Logger log = LoggerFactory.getLogger(MultiplePortsExposedTest.class); @Rule // rule { public GenericContainer container = new GenericContainer<>( DockerImageName.parse("testcontainers/helloworld:1.1.0") ) .withExposedPorts(8080, 8081) .withLogConsumer(new Slf4jLogConsumer(log)); // } @Test public void fetchPortsByNumber() { Integer firstMappedPort = container.getMappedPort(8080); Integer secondMappedPort = container.getMappedPort(8081); } @Test public void fetchFirstMappedPort() { Integer firstMappedPort = container.getFirstMappedPort(); } @Test public void getHostOnly() { String ipAddress = container.getHost(); } @Test public void getHostAndMappedPort() { String address = container.getHost() + ":" + container.getMappedPort(8080); } } ================================================ FILE: docs/examples/junit4/generic/src/test/java/generic/WaitStrategiesTest.java ================================================ package generic; import org.junit.Rule; import org.junit.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.containers.wait.strategy.WaitStrategy; import org.testcontainers.utility.DockerImageName; import static org.assertj.core.api.Assertions.assertThat; public class WaitStrategiesTest { @Rule // waitForNetworkListening { public GenericContainer nginx = new GenericContainer(DockerImageName.parse("nginx:1.27.0-alpine3.19-slim")) // .withExposedPorts(80); // } @Rule // waitForSimpleHttp { public GenericContainer nginxWithHttpWait = new GenericContainer( DockerImageName.parse("nginx:1.27.0-alpine3.19-slim") ) .withExposedPorts(80) .waitingFor(Wait.forHttp("/")); // } @Rule // logMessageWait { public GenericContainer containerWithLogWait = new GenericContainer(DockerImageName.parse("redis:6-alpine")) .withExposedPorts(6379) .waitingFor(Wait.forLogMessage(".*Ready to accept connections.*\\n", 1)); // } private static final HttpWaitStrategy MULTI_CODE_HTTP_WAIT = // spotless:off // waitForHttpWithMultipleStatusCodes { Wait.forHttp("/") .forStatusCode(200) .forStatusCode(301); // } // spotless:on private static final HttpWaitStrategy PREDICATE_HTTP_WAIT = // spotless:off // waitForHttpWithStatusCodePredicate { Wait.forHttp("/all") .forStatusCodeMatching(it -> it >= 200 && it < 300 || it == 401); // } // spotless:on private static final HttpWaitStrategy TLS_HTTP_WAIT = // spotless:off // waitForHttpWithTls { Wait.forHttp("/all") .usingTls(); // } // spotless:on private static final WaitStrategy HEALTHCHECK_WAIT = // spotless:off // healthcheckWait { Wait.forHealthcheck(); // } // spotless:on @Test public void testContainersAllStarted() { assertThat(nginx.isRunning()).isTrue(); assertThat(nginxWithHttpWait.isRunning()).isTrue(); assertThat(containerWithLogWait.isRunning()).isTrue(); } } ================================================ FILE: docs/examples/junit4/generic/src/test/java/generic/support/TestSpecificImageNameSubstitutor.java ================================================ package generic.support; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.ImageNameSubstitutor; /** * An {@link ImageNameSubstitutor} which makes it possible to use fake image names in * {@link generic.ImageNameSubstitutionTest}. This implementation simply reverses a fake image name when presented, and * is hardcoded to act upon the specific fake name in that test. */ public class TestSpecificImageNameSubstitutor extends ImageNameSubstitutor { @Override public DockerImageName apply(final DockerImageName original) { if (original.equals(DockerImageName.parse("registry.mycompany.com/mirror/mysql:8.0.36"))) { return DockerImageName.parse("mysql:8.0.36"); } else { return original; } } @Override protected String getDescription() { return TestSpecificImageNameSubstitutor.class.getSimpleName(); } } ================================================ FILE: docs/examples/junit4/generic/src/test/java/org/testcontainers/containers/startupcheck/StartupCheckStrategyTest.java ================================================ package org.testcontainers.containers.startupcheck; import lombok.SneakyThrows; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Suite; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.output.OutputFrame; import org.testcontainers.containers.output.WaitingConsumer; import org.testcontainers.utility.DockerImageName; import java.time.Duration; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import static org.assertj.core.api.Assertions.assertThat; @RunWith(Suite.class) @Suite.SuiteClasses( { StartupCheckStrategyTest.OneShotStrategyTest.class, StartupCheckStrategyTest.IndefiniteOneShotStrategyTest.class, StartupCheckStrategyTest.MinimumDurationStrategyTest.class, } ) public class StartupCheckStrategyTest { private static final String HELLO_TESTCONTAINERS = "Hello Testcontainers!"; private static void waitForHello(GenericContainer container) throws TimeoutException { WaitingConsumer consumer = new WaitingConsumer(); container.followOutput(consumer, OutputFrame.OutputType.STDOUT); consumer.waitUntil(frame -> frame.getUtf8String().contains(HELLO_TESTCONTAINERS), 30, TimeUnit.SECONDS); } public static class OneShotStrategyTest { @Rule // spotless:off // withOneShotStrategy { public GenericContainer bboxWithOneShot = new GenericContainer<>(DockerImageName.parse("busybox:1.31.1")) .withCommand(String.format("echo %s", HELLO_TESTCONTAINERS)) .withStartupCheckStrategy( new OneShotStartupCheckStrategy().withTimeout(Duration.ofSeconds(3)) ); // } // spotless:on @SneakyThrows @Test public void testCommandIsExecuted() { waitForHello(bboxWithOneShot); assertThat(bboxWithOneShot.isRunning()).isFalse(); } } public static class IndefiniteOneShotStrategyTest { @Rule // spotless:off // withIndefiniteOneShotStrategy { public GenericContainer bboxWithIndefiniteOneShot = new GenericContainer<>( DockerImageName.parse("busybox:1.31.1") ) .withCommand("sh", "-c", String.format("sleep 5 && echo \"%s\"", HELLO_TESTCONTAINERS)) .withStartupCheckStrategy( new IndefiniteWaitOneShotStartupCheckStrategy() ); // } // spotless:on @SneakyThrows @Test public void testCommandIsExecuted() { waitForHello(bboxWithIndefiniteOneShot); assertThat(bboxWithIndefiniteOneShot.isRunning()).isFalse(); } } public static class MinimumDurationStrategyTest { @Rule // spotless:off // withMinimumDurationStrategy { public GenericContainer bboxWithMinimumDuration = new GenericContainer<>( DockerImageName.parse("busybox:1.31.1") ) .withCommand("sh", "-c", String.format("sleep 5 && echo \"%s\"", HELLO_TESTCONTAINERS)) .withStartupCheckStrategy( new MinimumDurationRunningStartupCheckStrategy(Duration.ofSeconds(1)) ); // } // spotless:on @SneakyThrows @Test public void testCommandIsExecuted() { assertThat(bboxWithMinimumDuration.isRunning()).isTrue(); waitForHello(bboxWithMinimumDuration); } } } ================================================ FILE: docs/examples/junit4/generic/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n ================================================ FILE: docs/examples/junit4/generic/src/test/resources/testcontainers.properties ================================================ image.substitutor=generic.support.TestSpecificImageNameSubstitutor ================================================ FILE: docs/examples/junit4/redis/build.gradle ================================================ description = "Examples for docs" dependencies { api "io.lettuce:lettuce-core:6.8.0.RELEASE" testImplementation "junit:junit:4.13.2" testImplementation project(":testcontainers") testImplementation 'org.assertj:assertj-core:3.27.4' } ================================================ FILE: docs/examples/junit4/redis/src/main/java/quickstart/RedisBackedCache.java ================================================ package quickstart; import io.lettuce.core.RedisClient; import io.lettuce.core.api.StatefulRedisConnection; public class RedisBackedCache { private final StatefulRedisConnection connection; public RedisBackedCache(String hostname, Integer port) { RedisClient client = RedisClient.create(String.format("redis://%s:%d/0", hostname, port)); connection = client.connect(); } public String get(String key) { return connection.sync().get(key); } public void put(String key, String value) { connection.sync().set(key, value); } } ================================================ FILE: docs/examples/junit4/redis/src/test/java/quickstart/RedisBackedCacheIntTest.java ================================================ package quickstart; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.DockerImageName; import static org.assertj.core.api.Assertions.assertThat; public class RedisBackedCacheIntTest { private RedisBackedCache underTest; // rule { @Rule public GenericContainer redis = new GenericContainer(DockerImageName.parse("redis:6-alpine")) .withExposedPorts(6379); // } @Before public void setUp() { String address = redis.getHost(); Integer port = redis.getFirstMappedPort(); // Now we have an address and port for Redis, no matter where it is running underTest = new RedisBackedCache(address, port); } @Test public void testSimplePutAndGet() { underTest.put("test", "example"); String retrieved = underTest.get("test"); assertThat(retrieved).isEqualTo("example"); } } ================================================ FILE: docs/examples/junit4/redis/src/test/java/quickstart/RedisBackedCacheIntTestStep0.java ================================================ package quickstart; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; @Ignore("This test class is deliberately invalid, as it relies on a non-existent local Redis") public class RedisBackedCacheIntTestStep0 { private RedisBackedCache underTest; @Before public void setUp() { // Assume that we have Redis running locally? underTest = new RedisBackedCache("localhost", 6379); } @Test public void testSimplePutAndGet() { underTest.put("test", "example"); String retrieved = underTest.get("test"); assertThat(retrieved).isEqualTo("example"); } } ================================================ FILE: docs/examples/junit4/redis/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: docs/examples/junit5/redis/build.gradle ================================================ description = "Examples for docs" dependencies { api "io.lettuce:lettuce-core:6.8.0.RELEASE" testImplementation 'org.junit.jupiter:junit-jupiter-api:5.13.4' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.13.4' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.13.4' testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.3' testImplementation project(":testcontainers") testImplementation project(":testcontainers-junit-jupiter") testImplementation 'org.assertj:assertj-core:3.27.4' } test { useJUnitPlatform() } ================================================ FILE: docs/examples/junit5/redis/src/main/java/quickstart/RedisBackedCache.java ================================================ package quickstart; import io.lettuce.core.RedisClient; import io.lettuce.core.api.StatefulRedisConnection; public class RedisBackedCache { private final StatefulRedisConnection connection; public RedisBackedCache(String hostname, Integer port) { RedisClient client = RedisClient.create(String.format("redis://%s:%d/0", hostname, port)); connection = client.connect(); } public String get(String key) { return connection.sync().get(key); } public void put(String key, String value) { connection.sync().set(key, value); } } ================================================ FILE: docs/examples/junit5/redis/src/test/java/quickstart/RedisBackedCacheIntTest.java ================================================ package quickstart; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; import static org.assertj.core.api.Assertions.assertThat; // class { @Testcontainers public class RedisBackedCacheIntTest { private RedisBackedCache underTest; // container { @Container public GenericContainer redis = new GenericContainer(DockerImageName.parse("redis:6-alpine")) .withExposedPorts(6379); // } @BeforeEach public void setUp() { String address = redis.getHost(); Integer port = redis.getFirstMappedPort(); // Now we have an address and port for Redis, no matter where it is running underTest = new RedisBackedCache(address, port); } @Test public void testSimplePutAndGet() { underTest.put("test", "example"); String retrieved = underTest.get("test"); assertThat(retrieved).isEqualTo("example"); } } // } ================================================ FILE: docs/examples/junit5/redis/src/test/java/quickstart/RedisBackedCacheIntTestStep0.java ================================================ package quickstart; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @Disabled("This test class is deliberately invalid, as it relies on a non-existent local Redis") public class RedisBackedCacheIntTestStep0 { private RedisBackedCache underTest; @BeforeEach public void setUp() { // Assume that we have Redis running locally? underTest = new RedisBackedCache("localhost", 6379); } @Test public void testSimplePutAndGet() { underTest.put("test", "example"); String retrieved = underTest.get("test"); assertThat(retrieved).isEqualTo("example"); } } ================================================ FILE: docs/examples/junit5/redis/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n PROFILER DENY ================================================ FILE: docs/examples/spock/redis/build.gradle ================================================ plugins { id 'java' id 'groovy' } dependencies { api "io.lettuce:lettuce-core:6.8.0.RELEASE" testImplementation 'org.spockframework:spock-core:2.3-groovy-4.0' testImplementation project(":testcontainers-spock") testImplementation 'ch.qos.logback:logback-classic:1.3.15' } test { useJUnitPlatform() } ================================================ FILE: docs/examples/spock/redis/src/main/java/quickstart/RedisBackedCache.java ================================================ package quickstart; import io.lettuce.core.RedisClient; import io.lettuce.core.api.StatefulRedisConnection; public class RedisBackedCache { private final StatefulRedisConnection connection; public RedisBackedCache(String hostname, Integer port) { RedisClient client = RedisClient.create(String.format("redis://%s:%d/0", hostname, port)); connection = client.connect(); } public String get(String key) { return connection.sync().get(key); } public void put(String key, String value) { connection.sync().set(key, value); } } ================================================ FILE: docs/examples/spock/redis/src/test/groovy/quickstart/RedisBackedCacheIntTest.groovy ================================================ package quickstart import org.testcontainers.containers.GenericContainer import spock.lang.Specification // complete { @org.testcontainers.spock.Testcontainers class RedisBackedCacheIntTest extends Specification { private RedisBackedCache underTest // init { GenericContainer redis = new GenericContainer<>("redis:6-alpine") .withExposedPorts(6379) // } void setup() { String address = redis.host Integer port = redis.firstMappedPort // Now we have an address and port for Redis, no matter where it is running underTest = new RedisBackedCache(address, port) } void testSimplePutAndGet() { setup: underTest.put("test", "example") when: String retrieved = underTest.get("test") then: retrieved == "example" } } // } ================================================ FILE: docs/examples/spock/redis/src/test/groovy/quickstart/RedisBackedCacheIntTestStep0.groovy ================================================ package quickstart import spock.lang.Ignore import spock.lang.Specification @Ignore("This test class is deliberately invalid, as it relies on a non-existent local Redis") class RedisBackedCacheIntTestStep0 extends Specification { private RedisBackedCache underTest void setup() { // Assume that we have Redis running locally? underTest = new RedisBackedCache("localhost", 6379) } void testSimplePutAndGet() { setup: underTest.put("test", "example") when: String retrieved = underTest.get("test") then: retrieved == "example" } } ================================================ FILE: docs/examples/spock/redis/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: docs/examples.md ================================================ # Examples Examples of different use cases provided by Testcontainers can be found below: - [Hazelcast](https://github.com/testcontainers/testcontainers-java/tree/main/examples/hazelcast) - [Kafka Cluster with multiple brokers](https://github.com/testcontainers/testcontainers-java/tree/main/examples/kafka-cluster) - [Neo4j](https://github.com/testcontainers/testcontainers-java/tree/main/examples/neo4j-container) - [Redis](https://github.com/testcontainers/testcontainers-java/tree/main/examples/redis-backed-cache) - [Selenium](https://github.com/testcontainers/testcontainers-java/tree/main/examples/selenium-container) - [Selenium Module with Cucumber](https://github.com/testcontainers/testcontainers-java/tree/main/examples/cucumber) - [Singleton Container Pattern](https://github.com/testcontainers/testcontainers-java/tree/main/examples/singleton-container) - [Solr](https://github.com/testcontainers/testcontainers-java/tree/main/examples/solr-container) - [Spring Boot](https://github.com/testcontainers/testcontainers-java/tree/main/examples/spring-boot) - [Spring Boot with Kotlin](https://github.com/testcontainers/testcontainers-java/tree/main/examples/spring-boot-kotlin-redis) - [TestNG](https://github.com/testcontainers/testcontainers-java/tree/main/examples/redis-backed-cache-testng) - [ImmuDb](https://github.com/testcontainers/testcontainers-java/tree/main/examples/immudb) - [Zookeeper](https://github.com/testcontainers/testcontainers-java/tree/main/examples/zookeeper) - [NATS](https://github.com/testcontainers/testcontainers-java/tree/main/examples/nats) - [SFTP](https://github.com/testcontainers/testcontainers-java/tree/main/examples/sftp) ================================================ FILE: docs/features/advanced_options.md ================================================ # Advanced options ## Container labels To add a custom label to the container, use `withLabel`: [Adding a single label](../examples/junit4/generic/src/test/java/generic/ContainerLabelTest.java) inside_block:single_label Additionally, multiple labels may be applied together from a map: [Adding multiple labels](../examples/junit4/generic/src/test/java/generic/ContainerLabelTest.java) inside_block:multiple_labels ## Image Pull Policy By default, the container image is retrieved from the local Docker images cache. This works well when running against a specific version, but for images with a static tag (i.e. 'latest') this may lead to a newer version not being pulled. It is possible to specify an Image Pull Policy to determine at runtime whether an image should be pulled or not: [Setting image pull policy](../../core/src/test/java/org/testcontainers/images/ImagePullPolicyTest.java) inside_block:built_in_image_pull_policy ... or providing a function: [Custom image pull policy](../../core/src/test/java/org/testcontainers/images/ImagePullPolicyTest.java) inside_block:custom_image_pull_policy You can also configure Testcontainers to use your custom implementation by using `pull.policy` === "`src/test/resources/testcontainers.properties`" ```text pull.policy=com.mycompany.testcontainers.ExampleImagePullPolicy ``` You can also use the provided implementation to always pull images === "`src/test/resources/testcontainers.properties`" ```text pull.policy=org.testcontainers.images.AlwaysPullPolicy ``` Please see [the documentation on configuration mechanisms](./configuration.md) for more information. ## Customizing the container ### Using docker-java It is possible to use the [`docker-java`](https://github.com/docker-java/docker-java) API directly to customize containers before creation. This is useful if there is a need to use advanced Docker features that are not exposed by the Testcontainers API. Any customizations you make using `withCreateContainerCmdModifier` will be applied _on top_ of the container definition that Testcontainers creates, but before it is created. For example, this can be used to change the container hostname: [Using modifier to change hostname](../examples/junit4/generic/src/test/java/generic/CmdModifierTest.java) inside_block:hostname ... or modify container memory (see [this](https://fabiokung.com/2014/03/13/memory-inside-linux-containers/) if it does not appear to work): [Using modifier to change memory limits](../examples/junit4/generic/src/test/java/generic/CmdModifierTest.java) inside_block:memory !!! note It is recommended to use this sparingly, and follow changes to the `docker-java` API if you choose to use this. It is typically quite stable, though. For what is possible, consult the [`docker-java CreateContainerCmd` source code](https://github.com/docker-java/docker-java/blob/3.2.1/docker-java-api/src/main/java/com/github/dockerjava/api/command/CreateContainerCmd.java). ### Using CreateContainerCmdModifier Testcontainers provides a `CreateContainerCmdModifier` to customize [`docker-java CreateContainerCmd`](https://github.com/docker-java/docker-java/blob/3.2.1/docker-java-api/src/main/java/com/github/dockerjava/api/command/CreateContainerCmd.java) via Service Provider Interface (SPI) mechanism. [CreateContainerCmd example implementation](../../core/src/test/java/org/testcontainers/custom/TestCreateContainerCmdModifier.java) The previous implementation should be registered in `META-INF/services/org.testcontainers.core.CreateContainerCmdModifier` file. !!! warning `CreateContainerCmdModifier` implementation will apply to all containers created by Testcontainers. ## Parallel Container Startup Usually, containers are started sequentially when more than one container is used. Using `Startables.deepStart(container1, container2, ...).join()` will start all containers in parallel. This can be advantageous to reduce the impact of the container startup overhead. ================================================ FILE: docs/features/commands.md ================================================ # Executing commands ## Container startup command By default the container will execute whatever command is specified in the image's Dockerfile. To override this, and specify a different command, use `withCommand`. For example: [Specifying a startup command](../examples/junit4/generic/src/test/java/generic/CommandsTest.java) inside_block:startupCommand ## Executing a command Your test can execute a command inside a running container, similar to a `docker exec` call: [Executing a command inside a running container](../examples/junit4/generic/src/test/java/generic/ExecTest.java) inside_block:standaloneExec This can be useful for software that has a command line administration tool. You can also get the output (stdout/stderr) and exit code from the command - for example: [Executing a command inside a running container and reading the result](../examples/junit4/generic/src/test/java/generic/ExecTest.java) inside_block:execReadingStdout ## Environment variables To add environment variables to the container, use `withEnv`: ```java new GenericContainer(...) .withEnv("API_TOKEN", "foo") ``` ================================================ FILE: docs/features/configuration.md ================================================ # Custom configuration You can override some default properties if your environment requires that. ## Configuration locations The configuration will be loaded from multiple locations. Properties are considered in the following order: 1. Environment variables 2. `.testcontainers.properties` in user's home folder. Example locations: **Linux:** `/home/myuser/.testcontainers.properties` **Windows:** `C:/Users/myuser/.testcontainers.properties` **macOS:** `/Users/myuser/.testcontainers.properties` 3. `testcontainers.properties` on the classpath. Note that when using environment variables, configuration property names should be set in upper case with underscore separators, preceded by `TESTCONTAINERS_` - e.g. `checks.disable` becomes `TESTCONTAINERS_CHECKS_DISABLE`. The classpath `testcontainers.properties` file may exist within the local codebase (e.g. within the `src/test/resources` directory) or within library dependencies that you may have. Any such configuration files will have their contents merged. If any keys conflict, the value will be taken on the basis of the first value found in: * 'local' classpath (i.e. where the URL of the file on the classpath begins with `file:`), then * other classpath locations (i.e. JAR files) - considered in _alphabetical order of path_ to provide deterministic ordering. ## Disabling the startup checks > **checks.disable = [true|false]** Before running any containers Testcontainers will perform a set of startup checks to ensure that your environment is configured correctly. Usually they look like this: ``` ℹ︎ Checking the system... ✔ Docker version should be at least 1.6.0 ✔ File should be mountable ✔ A port exposed by a docker container should be accessible ``` It takes a couple of seconds, but if you want to speed up your tests, you can disable the checks once you have everything configured. Add `checks.disable=true` to your `$HOME/.testcontainers.properties` to completely disable them. ## Customizing images !!! note This approach is discouraged and deprecated, but is documented for completeness. Overriding individual image names via configuration may be removed in 2021. See [Image Name Substitution](./image_name_substitution.md) for other strategies for substituting image names to pull from other registries. Testcontainers uses public Docker images to perform different actions like startup checks, VNC recording and others. Some companies disallow the usage of Docker Hub, but you can override `*.image` properties with your own images from your private registry to workaround that. > **ryuk.container.image = testcontainers/ryuk:0.3.3** > Performs fail-safe cleanup of containers, and always required (unless [Ryuk is disabled](#disabling-ryuk)) > **tinyimage.container.image = alpine:3.17** > Used to check whether images can be pulled at startup, and always required (unless [startup checks are disabled](#disabling-the-startup-checks)) > **sshd.container.image = testcontainers/sshd:1.1.0** > Required if [exposing host ports to containers](./networking.md#exposing-host-ports-to-the-container) > **vncrecorder.container.image = testcontainers/vnc-recorder:1.3.0** > Used by VNC recorder in Testcontainers' Selenium integration > **socat.container.image = alpine/socat** > **compose.container.image = docker/compose:1.8.0** > Required if using [Docker Compose](../modules/docker_compose.md) > **kafka.container.image = confluentinc/cp-kafka** > Used by KafkaContainer > **localstack.container.image = localstack/localstack** > Used by LocalStack > **pulsar.container.image = apachepulsar/pulsar:2.2.0** > Used by Apache Pulsar ## Customizing Ryuk resource reaper > **ryuk.container.image = testcontainers/ryuk:0.3.3** > The resource reaper is responsible for container removal and automatic cleanup of dead containers at JVM shutdown > **ryuk.container.privileged = true** > In some environments ryuk must be started in privileged mode to work properly (--privileged flag) ### Disabling Ryuk Ryuk must be started as a privileged container. If your environment already implements automatic cleanup of containers after the execution, but does not allow starting privileged containers, you can turn off the Ryuk container by setting `TESTCONTAINERS_RYUK_DISABLED` **environment variable** to `true`. !!!tip Note that Testcontainers will continue doing the cleanup at JVM's shutdown, unless you `kill -9` your JVM process. ## Customizing image pull behaviour > **pull.timeout = 120** > By default Testcontainers will timeout if pull takes more than this duration (in seconds) > **pull.pause.timeout = 30** > By default Testcontainers will abort the pull of an image if the pull appears stalled (no data transferred) for longer than this duration (in seconds). ## Customizing client ping behaviour > **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 Testcontainers will attempt to detect the Docker environment and configure everything to work automatically. However, sometimes customization is required. Testcontainers will respect the following **environment variables**: > **DOCKER_HOST** = unix:///var/run/docker.sock > See [Docker environment variables](https://docs.docker.com/engine/reference/commandline/cli/#environment-variables) > > **TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE** > Path to Docker's socket. Used by Ryuk, Docker Compose, and a few other containers that need to perform Docker actions. > Example: `/var/run/docker-alt.sock` > > **TESTCONTAINERS_HOST_OVERRIDE** > Docker's host on which ports are exposed. > Example: `docker.svc.local` For advanced users, the Docker host connection can be configured **via configuration** in `~/.testcontainers.properties`. Note that these settings require use of the `EnvironmentAndSystemPropertyClientProviderStrategy`. The example below illustrates usage: ```properties docker.client.strategy=org.testcontainers.dockerclient.EnvironmentAndSystemPropertyClientProviderStrategy docker.host=tcp\://my.docker.host\:1234 # Equivalent to the DOCKER_HOST environment variable. Colons should be escaped. docker.tls.verify=1 # Equivalent to the DOCKER_TLS_VERIFY environment variable docker.cert.path=/some/path # Equivalent to the DOCKER_CERT_PATH environment variable ``` In addition, you can deactivate this behaviour by specifying: ```properties dockerconfig.source=autoIgnoringUserProperties # 'auto' by default ``` ================================================ FILE: docs/features/container_logs.md ================================================ # Accessing container logs It is possible to capture container output using: * the `getLogs()` method, which simply returns a `String` snapshot of a container's entire log output * the `followOutput()` method. This method accepts a Consumer and (optionally) a varargs list stating which of STDOUT, STDERR, or both, should be followed. If not specified, both will be followed. At present, container output will always begin from the time of container creation. ## Reading all logs (from startup time to present) `getLogs()` is the simplest mechanism for accessing container logs, and can be used as follows: [Accessing all output (stdout and stderr)](../../core/src/test/java/org/testcontainers/containers/output/ContainerLogsTest.java) inside_block:docsGetAllLogs [Accessing just stdout](../../core/src/test/java/org/testcontainers/containers/output/ContainerLogsTest.java) inside_block:docsGetStdOut [Accessing just stderr](../../core/src/test/java/org/testcontainers/containers/output/ContainerLogsTest.java) inside_block:docsGetStdErr ## Streaming logs Testcontainers includes some out-of-the-box Consumer implementations that can be used with the streaming `followOutput()` model; examples follow. ### Streaming container output to an SLF4J logger Given an existing SLF4J logger instance named LOGGER: ```java Slf4jLogConsumer logConsumer = new Slf4jLogConsumer(LOGGER); container.followOutput(logConsumer); ``` By default both standard out and standard error will both be emitted at INFO level. Standard error may be emitted at ERROR level, if desired: ```java Slf4jLogConsumer logConsumer = new Slf4jLogConsumer(LOGGER).withSeparateOutputStreams(); ``` The [Mapped Diagnostic Context (MDC)](http://logback.qos.ch/manual/mdc.html) for emitted messages may be configured using the `withMdc(...)` option: ```java Slf4jLogConsumer logConsumer = new Slf4jLogConsumer(LOGGER).withMdc("key", "value"); ``` or using an existing map of key-value pairs: ```java Slf4jLogConsumer logConsumer = new Slf4jLogConsumer(LOGGER).withMdc(map); ``` ### Capturing container output as a String To stream logs live or customize the decoding, `ToStringConsumer` may be used: ```java ToStringConsumer toStringConsumer = new ToStringConsumer(); container.followOutput(toStringConsumer, OutputType.STDOUT); String utf8String = toStringConsumer.toUtf8String(); // Or if the container output is not UTF-8 String otherString = toStringConsumer.toString(CharSet.forName("ISO-8859-1")); ``` ### Waiting for container output to contain expected content `WaitingConsumer` will block until a frame of container output (usually a line) matches a provided predicate. A timeout may be specified, as shown in this example. ```java WaitingConsumer consumer = new WaitingConsumer(); container.followOutput(consumer, STDOUT); consumer.waitUntil(frame -> frame.getUtf8String().contains("STARTED"), 30, TimeUnit.SECONDS); ``` Additionally, as the Java 8 Consumer functional interface is used, Consumers may be composed together. This is useful, for example, to capture all the container output but only when a matching string has been found. e.g.: ```java WaitingConsumer waitingConsumer = new WaitingConsumer(); ToStringConsumer toStringConsumer = new ToStringConsumer(); Consumer composedConsumer = toStringConsumer.andThen(waitingConsumer); container.followOutput(composedConsumer); waitingConsumer.waitUntil(frame -> frame.getUtf8String().contains("STARTED"), 30, TimeUnit.SECONDS); String utf8String = toStringConsumer.toUtf8String(); ``` ================================================ FILE: docs/features/creating_container.md ================================================ # Creating a container ## Creating a generic container based on an image Testcontainers' generic container support offers the most flexibility, and makes it easy to use virtually any container images as temporary test dependencies. For example, if you might use it to test interactions with: * NoSQL databases or other data stores (e.g. redis, elasticsearch, mongo) * Web servers/proxies (e.g. nginx, apache) * Log services (e.g. logstash, kibana) * Other services developed by your team/organization which are already dockerized With a generic container, you set the container image using a parameter to the rule constructor, e.g.: ```java new GenericContainer(DockerImageName.parse("jboss/wildfly:9.0.1.Final")) ``` ### Specifying an image Many Container classes in Testcontainers have historically supported: * a no-args constructor - for example `new GenericContainer()` and `new ElasticsearchContainer()`. With these constructors, Testcontainers has traditionally used a default image name (including a fixed image tag/version). This has caused a conflict between the need to keep the defaults sane (i.e. up to date) and the need to avoid silently upgrading these dependencies along with new versions of Testcontainers. * a single string-argument constructor, which has taken either a version or an image name as a String. This has caused some ambiguity and confusion. Since v1.15.0, both of these constructor types have been deprecated, for the reasons given above. Instead, it is highly recommended that _all containers_ be constructed using a constructor that accepts a `DockerImageName` object. The `DockerImageName` class is an unambiguous reference to a docker image. It is suggested that developers treat `DockerImageName`s as you would any other potentially-constant value - consider defining a constant in your test codebase that matches the production version of the dependency you are using. ### Examples A generic container rule can be used with any public docker image; for example: [Creating a Redis container (JUnit 4)](../examples/junit4/generic/src/test/java/generic/ContainerCreationTest.java) inside_block:simple Further options may be specified: [Creating a container with more options (JUnit 4)](../examples/junit4/generic/src/test/java/generic/ContainerCreationTest.java) inside_block:withOptions These containers, as `@ClassRule`s, will be started before any tests in the class run, and will be destroyed after all tests have run. ================================================ FILE: docs/features/creating_images.md ================================================ # Creating images on-the-fly ## Overview In situations where there is no pre-existing Docker image, Testcontainers can create a new temporary image on-the-fly from a Dockerfile. For example, when the component under test is the Docker image itself, or when an existing base image needs to be customized for specific test(s). Simply pass a configured instance of `ImageFromDockerfile` as a constructor parameter to `GenericContainer`. Testcontainers will `docker build` a temporary container image, and will use it when creating the container. ## Dockerfile from String, file or classpath resource `ImageFromDockerfile` accepts arbitrary files, strings or classpath resources to be used as files in the build context. At least one of these needs to be a `Dockerfile`. ```java @Rule public GenericContainer dslContainer = new GenericContainer( new ImageFromDockerfile() .withFileFromString("folder/someFile.txt", "hello") .withFileFromClasspath("test.txt", "mappable-resource/test-resource.txt") .withFileFromClasspath("Dockerfile", "mappable-dockerfile/Dockerfile")) ``` The following methods may be used to provide the `Dockerfile` and any other required build context files: * `withFileFromString(buildContextPath, content)` * `withFileFromClasspath(buildContextPath, classpathPath)` * `withFileFromPath(buildContextPath, filesystemPath)` * `withFileFromFile(buildContextPath, filesystemFile)` !!! info In older versions of Testcontainers (before 1.3.0) it was necessary to explicitly declare each file that needed to be present in the Docker build context. This can be replaced with the following syntax: [Passing an entire directory of files to the Dockerfile build context](../../core/src/test/java/org/testcontainers/images/builder/DockerfileBuildTest.java) inside_block:docsShowRecursiveFileInclusion Where `RESOURCE_PATH` is the path to a directory containing a `Dockerfile` and any files that it needs to refer to. Doing this is equivalent to `docker build RESOURCE_PATH` on the command line. To mimic `docker build .`, `RESOURCE_PATH` would simply be set to `.` as well. ## Dockerfile DSL If a static Dockerfile is not sufficient (e.g. your test needs to cover many variations that are best generated programmatically), there is a DSL available to allow Dockerfiles to be defined in code. e.g.: ```java new GenericContainer( new ImageFromDockerfile() .withDockerfileFromBuilder(builder -> builder .from("alpine:3.17") .run("apk add --update nginx") .cmd("nginx", "-g", "daemon off;") .build())) .withExposedPorts(80); ``` See `ParameterizedDockerfileContainerTest` for a very basic example of using this in conjunction with JUnit parameterized testing. ## Automatic deletion Temporary container images will be automatically removed when the test JVM shuts down. If this is not desired and the image should be retained between tests, pass a stable image name and `false` flag to the `ImageFromDockerfile` constructor. Retaining the image between tests will use Docker's image cache to accelerate subsequent test runs. By default the no-args constructor will use an image name of the form `testcontainers/` + random string: * `public ImageFromDockerfile()` * `public ImageFromDockerfile(String dockerImageName)` * `public ImageFromDockerfile(String dockerImageName, boolean deleteOnExit)` ## Alternative Dockerfiles Normally Docker will automatically build an image from any `/Dockerfile` that it finds in the root of the build context. To override this behaviour, use `.withDockerfilePath("./Name-Of-Other-Dockerfile")`. ## Build Args [Build Args](https://docs.docker.com/engine/reference/builder/#arg) may be used to allow lightweight parameterization. To specify build args, use `.withBuildArg("varname", "value")` or provide a `Map` of args using `.withBuildArgs(map)`. ================================================ FILE: docs/features/files.md ================================================ # Files and volumes ## Copying files Files can be copied into the container before startup, or can be copied from the container after the container has started. !!! note This is the recommended approach for portability cross-docker environments. ### Copying to a container before startup [Copying files using MountableFile](../../core/src/test/java/org/testcontainers/junit/CopyFileToContainerTest.java) inside_block:copyToContainer Using `Transferable`, file content will be placed in the specified location. [Copying files using Transferable](../../core/src/test/java/org/testcontainers/containers/GenericContainerTest.java) inside_block:transferableFile Setting file mode is also possible. [Copying files using Transferable with file mode](../../core/src/test/java/org/testcontainers/containers/GenericContainerTest.java) inside_block:transferableWithFileMode ### Copying a file from a running container [Copying files from a container](../../core/src/test/java/org/testcontainers/junit/CopyFileToContainerTest.java) inside_block:copyFileFromContainer ================================================ FILE: docs/features/image_name_substitution.md ================================================ # Image name substitution Testcontainers supports automatic substitution of Docker image names. This allows replacement of an image name specified in test code with an alternative name - for example, to replace the name of a Docker Hub image dependency with an alternative hosted on a private image registry. This is advisable to avoid [Docker Hub rate limiting](../supported_docker_environment/image_registry_rate_limiting.md), and some companies will prefer this for policy reasons. This page describes four approaches for image name substitution: * [Manual substitution](#manual-substitution) - not relying upon an automated approach * Using an Image Name Substitutor: * [Developing a custom function for transforming image names on the fly](#developing-a-custom-function-for-transforming-image-names-on-the-fly) * [Overriding image names individually in configuration](#overriding-image-names-individually-in-configuration) It is assumed that you have already set up a private registry hosting [all the Docker images your build requires](../supported_docker_environment/image_registry_rate_limiting.md#which-images-are-used-by-testcontainers). ## Manual substitution Consider this if: * You use only a few images and updating code is not a chore * All developers and CI machines in your organisation have access to a common registry server * You also use one of the automated mechanisms to substitute [the images that Testcontainers itself requires](../supported_docker_environment/image_registry_rate_limiting.md#which-images-are-used-by-testcontainers) This approach simply entails modifying test code manually, e.g. changing: For example, you may have a test that uses the `mysql` container image from Docker Hub: [Direct Docker Hub image name](../examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java) inside_block:directDockerHubReference to: [Private registry image name](../examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java) inside_block:hardcodedMirror ## Automatically modifying Docker Hub image names Testcontainers can be configured to modify Docker Hub image names on the fly to apply a prefix string. Consider this if: * Developers and CI machines need to use different image names. For example, developers are able to pull images from Docker Hub, but CI machines need to pull from a private registry * Your private registry has copies of images from Docker Hub where the names are predictable, and just adding a prefix is enough. For example, `registry.mycompany.com/mirror/mysql:8.0.36` can be derived from the original Docker Hub image name (`mysql:8.0.36`) with a consistent prefix string: `registry.mycompany.com/mirror/` In this case, image name references in code are **unchanged**. i.e. you would leave as-is: [Unchanged direct Docker Hub image name](../examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java) inside_block:directDockerHubReference You can then configure Testcontainers to apply the prefix `registry.mycompany.com/mirror/` to every image that it tries to pull from Docker Hub. This can be done in one of two ways: * Setting environment variables `TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX=registry.mycompany.com/mirror/` * Via config file, setting `hub.image.name.prefix` in either: * the `~/.testcontainers.properties` file in your user home directory, or * a file named `testcontainers.properties` on the classpath Testcontainers will automatically apply the prefix to every image that it pulls from Docker Hub - please verify that all [the required images](../supported_docker_environment/image_registry_rate_limiting.md#which-images-are-used-by-testcontainers) exist in your registry. Testcontainers will not apply the prefix to: * non-Hub image names (e.g. where another registry is set) * Docker Hub image names where the hub registry is explicitly part of the name (i.e. anything with a `docker.io` or `registry.hub.docker.com` host part) ## Developing a custom function for transforming image names on the fly Consider this if: * You have complex rules about which private registry images should be used as substitutes, e.g.: * non-deterministic mapping of names meaning that a [name prefix](#automatically-modifying-docker-hub-image-names) cannot be used * rules depending upon developer identity or location * or you wish to add audit logging of images used in the build * or you wish to prevent accidental usage of images that are not on an approved list In this case, image name references in code are **unchanged**. i.e. you would leave as-is: [Unchanged direct Docker Hub image name](../examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java) inside_block:directDockerHubReference You can implement a custom image name substitutor by: * subclassing `org.testcontainers.utility.ImageNameSubstitutor` * configuring Testcontainers to use your custom implementation The following is an example image substitutor implementation: [Example Image Substitutor](../examples/junit4/generic/src/test/java/generic/ExampleImageNameSubstitutor.java) block:ExampleImageNameSubstitutor Testcontainers can be configured to find it at runtime via configuration. To do this, create or modify a file on the classpath named `testcontainers.properties`. For example: === "`src/test/resources/testcontainers.properties`" ```text image.substitutor=com.mycompany.testcontainers.ExampleImageNameSubstitutor ``` Note that it is also possible to provide this same configuration property: * in a `testcontainers.properties` file at the root of a library JAR file (useful if you wish to distribute a drop-in image substitutor JAR within an organization) * in a properties file in the user's home directory (`~/.testcontainers.properties`; note the leading `.`) * or as an environment variable (e.g. `TESTCONTAINERS_IMAGE_SUBSTITUTOR=com.mycompany.testcontainers.ExampleImageNameSubstitutor`). Please see [the documentation on configuration mechanisms](./configuration.md) for more information. Also, you can use the `ServiceLoader` mechanism to provide the fully qualified class name of the `ImageNameSubstitutor` implementation: === "`src/test/resources/META-INF/services/org.testcontainers.utility.ImageNameSubstitutor`" ```text com.mycompany.testcontainers.ExampleImageNameSubstitutor ``` ## Overriding image names individually in configuration !!! note This approach is discouraged and deprecated, but is documented for completeness. Please consider one of the other approaches outlined in this page instead. Overriding individual image names via configuration may be removed in the future. Consider this if: * You have many references to image names in code and changing them is impractical, and * None of the other options are practical for you In this case, image name references in code are left **unchanged**. i.e. you would leave as-is: [Unchanged direct Docker Hub image name](../examples/junit4/generic/src/test/java/generic/ImageNameSubstitutionTest.java) inside_block:directDockerHubReference You can force Testcontainers to substitute in a different image [using a configuration file](./configuration.md), which allows some (but not all) container names to be substituted. ================================================ FILE: docs/features/jib.md ================================================ # Using Jib [Jib](https://github.com/GoogleContainerTools/jib/tree/master/jib-core) is a library for building Docker images. You can use it as an alternative to Testcontainers default `DockerfileBuilder`. [GenericContainer with JibImage](../../core/src/test/java/org/testcontainers/containers/JibTest.java) inside_block:jibContainerUsage !!! hint The Testcontainers library JAR will not automatically add a `jib-core` JAR to your project. Minimum version required is `com.google.cloud.tools:jib-core:0.22.0`. ================================================ FILE: docs/features/networking.md ================================================ # Networking and communicating with containers ## Exposing container ports to the host It is common to want to connect to a container from your test process, running on the test 'host' machine. For example, you may be testing a class that needs to connect to a backend or data store container. Generally, each required port needs to be explicitly exposed. For example, we can specify one or more ports as follows: [Exposing ports](../examples/junit4/generic/src/test/java/generic/MultiplePortsExposedTest.java) inside_block:rule Note that this exposed port number is from the *perspective of the container*. *From the host's perspective* Testcontainers actually exposes this on a random free port. This is by design, to avoid port collisions that may arise with locally running software or in between parallel test runs. Because there is this layer of indirection, it is necessary to ask Testcontainers for the actual mapped port at runtime. This can be done using the `getMappedPort` method, which takes the original (container) port as an argument: [Retrieving actual ports at runtime](../examples/junit4/generic/src/test/java/generic/MultiplePortsExposedTest.java) inside_block:fetchPortsByNumber !!! warning Because the randomised port mapping happens during container startup, the container must be running at the time `getMappedPort` is called. You may need to ensure that the startup order of components in your tests caters for this. There is also a `getFirstMappedPort` method for convenience, for the fairly common scenario of a container that only exposes one port: [Retrieving the first mapped port](../examples/junit4/generic/src/test/java/generic/MultiplePortsExposedTest.java) inside_block:fetchFirstMappedPort ## Getting the container host When running with a local Docker daemon, exposed ports will usually be reachable on `localhost`. However, in some CI environments they may instead be reachable on a different host. As such, Testcontainers provides a convenience method to obtain an address on which the container should be reachable from the host machine. [Getting the container host](../examples/junit4/generic/src/test/java/generic/MultiplePortsExposedTest.java) inside_block:getHostOnly It is normally advisable to use `getHost` and `getMappedPort` together when constructing addresses - for example: [Getting the container host and mapped port](../examples/junit4/generic/src/test/java/generic/MultiplePortsExposedTest.java) inside_block:getHostAndMappedPort !!! tip `getHost()` is a replacement for `getContainerIpAddress()` and returns the same result. `getContainerIpAddress()` is believed to be confusingly named, and will eventually be deprecated. ## Exposing host ports to the container In some cases it is necessary to make a network connection from a container to a socket that is listening on the host machine. Natively, Docker has limited support for this model across platforms. Testcontainers, however, makes this possible. In this example, assume that `localServerPort` is a port on our test host machine where a server (e.g. a web application) is running. We need to tell Testcontainers to prepare to expose this port to containers: [Exposing the host port](../examples/junit4/generic/src/test/java/generic/HostPortExposedTest.java) inside_block:exposePort !!! warning Note that the above command should be invoked _before_ containers are started, but _after_ the server on the host was started. Alternatively, use `container.withAccessToHost(true)` to force the host access mechanism (you still need to call `exposeHostPorts` to make the port available). Having done so, we can now access this port from any containers that are launched. From a container's perspective, the hostname will be `host.testcontainers.internal` and the port will be the same value as `localServerPort`. For example, here we construct an HTTP URL for our local web application and tell a Selenium container to get a page from it: [Accessing the exposed host port from a container](../examples/junit4/generic/src/test/java/generic/HostPortExposedTest.java) inside_block:useHostExposedPort ## Advanced networking Docker provides the ability for you to create custom networks and place containers on one or more networks. Then, communication can occur between networked containers without the need of exposing ports through the host. With Testcontainers, you can do this as well. !!! warning Note that Testcontainers currently only allows a container to be on a single network. [Creating custom networks](../../core/src/test/java/org/testcontainers/containers/NetworkTest.java) inside_block:useCustomNetwork ================================================ FILE: docs/features/reuse.md ================================================ # Reusable Containers (Experimental) !!! warning Reusable Containers is still an experimental feature and the behavior can change. Those containers won't stop after all tests are finished. The *Reusable* feature keeps the containers running and next executions with the same container configuration will reuse it. To use it, start the container manually by calling `start()` method, do not call `stop()` method directly or indirectly via `try-with-resources` or `JUnit integration`, and enable it manually through an opt-in mechanism per environment. To reuse a container, the container configuration **must be the same**. !!! note Reusable containers are not suited for CI usage and as an experimental feature not all Testcontainers features are fully working (e.g., resource cleanup or networking). ## How to use it * Enable `Reusable Containers` * through environment variable `TESTCONTAINERS_REUSE_ENABLE=true` * through user property file `~/.testcontainers.properties`, by adding `testcontainers.reuse.enable=true` * **not** through classpath properties file [see this comment](https://github.com/testcontainers/testcontainers-java/issues/5364#issuecomment-1125907734) * Define a container and subscribe to reuse the container using `withReuse(true)` ```java GenericContainer container = new GenericContainer("redis:6-alpine") .withExposedPorts(6379) .withReuse(true) ``` * Start the container manually by using `container.start()` ### Reusable Container with Testcontainers JDBC URL If using the [Testcontainers JDBC URL support](../../modules/databases/jdbc#database-containers-launched-via-jdbc-url-scheme) the URL **must** follow the pattern of `jdbc:tc:mysql:8.0.36:///databasename?TC_REUSABLE=true`. `TC_REUSABLE=true` is set as a parameter of the JDBC URL. ================================================ FILE: docs/features/startup_and_waits.md ================================================ # Waiting for containers to start or be ready !!! info "Wait strategies vs Startup strategies" **Wait strategy:** is the container in a state that is useful for testing. This is generally approximated as 'can we talk to this container over the network'. However, there are quite a few variations and nuances. **Startup strategy:** did a container reach the desired running state. *Almost always* this just means 'wait until the container is running' - for a daemon process in a container this is the goal. Sometimes we need to wait until the container reaches a running state and then exits - this is the 'one shot startup' strategy, only used for cases where we need to run a one off command in a container but not a daemon. ## Wait Strategies Ordinarily Testcontainers will wait for up to 60 seconds for the container's first mapped network port to start listening. This simple measure provides a basic check whether a container is ready for use. [Waiting for the first exposed port to start listening](../examples/junit4/generic/src/test/java/generic/WaitStrategiesTest.java) inside_block:waitForNetworkListening If the default 60s timeout is not sufficient, it can be altered with the `withStartupTimeout()` method. If waiting for a listening TCP port is not sufficient to establish whether the container is ready, you can use the `waitingFor()` method with other [`WaitStrategy`](http://static.javadoc.io/org.testcontainers/testcontainers/{{ latest_version }}/org/testcontainers/containers/wait/strategy/WaitStrategy.html) implementations as shown below. ### HTTP Wait strategy examples You can choose to wait for an HTTP(S) endpoint to return a particular status code. #### Waiting for 200 OK [](../examples/junit4/generic/src/test/java/generic/WaitStrategiesTest.java) inside_block:waitForSimpleHttp Variations on the HTTP wait strategy are supported, including: #### Waiting for multiple possible status codes [](../examples/junit4/generic/src/test/java/generic/WaitStrategiesTest.java) inside_block:waitForHttpWithMultipleStatusCodes #### Waiting for a status code that matches a predicate [Waiting for a status code that matches a predicate](../examples/junit4/generic/src/test/java/generic/WaitStrategiesTest.java) inside_block:waitForHttpWithStatusCodePredicate #### Using TLS [](../examples/junit4/generic/src/test/java/generic/WaitStrategiesTest.java) inside_block:waitForHttpWithTls ### Healthcheck Wait strategy examples If the used image supports Docker's [Healthcheck](https://docs.docker.com/engine/reference/builder/#healthcheck) feature, you can directly leverage the `healthy` state of the container as your wait condition: [](../examples/junit4/generic/src/test/java/generic/WaitStrategiesTest.java) inside_block:healthcheckWait ### Log output Wait Strategy In some situations a container's log output is a simple way to determine if it is ready or not. For example, we can wait for a `Ready' message in the container's logs as follows: [](../examples/junit4/generic/src/test/java/generic/WaitStrategiesTest.java) inside_block:logMessageWait ### Other Wait Strategies For further options, check out the [`Wait`](http://static.javadoc.io/org.testcontainers/testcontainers/{{ latest_version }}/org/testcontainers/containers/wait/strategy/Wait.html) convenience class, or the various subclasses of [`WaitStrategy`](http://static.javadoc.io/org.testcontainers/testcontainers/{{ latest_version }}/org/testcontainers/containers/wait/strategy/WaitStrategy.html). If none of these options meet your requirements, you can create your own subclass of [`AbstractWaitStrategy`](http://static.javadoc.io/org.testcontainers/testcontainers/{{ latest_version }}/org/testcontainers/containers/wait/strategy/AbstractWaitStrategy.html) with an appropriate wait mechanism in `waitUntilReady()`. The `GenericContainer.waitingFor()` method accepts any valid [`WaitStrategy`](http://static.javadoc.io/org.testcontainers/testcontainers/{{ latest_version }}/org/testcontainers/containers/wait/strategy/WaitStrategy.html). ## Startup check Strategies Ordinarily Testcontainers will check that the container has reached the running state and has not exited. In order to do that inspect is executed against the container and state parameter is extracted. All logic is implemented in [`StartupCheckStrategy`](http://static.javadoc.io/org.testcontainers/testcontainers/{{ latest_version }}/org/testcontainers/containers/startupcheck/StartupCheckStrategy.html) child classes. ### Running startup strategy example This is the strategy used by default. Testcontainers just checks if container is running. Implemented in [`IsRunningStartupCheckStrategy`](http://static.javadoc.io/org.testcontainers/testcontainers/{{ latest_version }}/org/testcontainers/containers/startupcheck/IsRunningStartupCheckStrategy.html) class. ### One shot startup strategy example This strategy is intended for use with containers that only run briefly and exit of their own accord. As such, success is deemed to be when the container has stopped with exit code 0. [Using one shot startup strategy](../examples/junit4/generic/src/test/java/org/testcontainers/containers/startupcheck/StartupCheckStrategyTest.java) inside_block:withOneShotStrategy ### Indefinite one shot startup strategy example Variant of one shot strategy that does not impose a timeout. Intended for situation such as when a long running task forms part of container startup. It has to be assumed that the container will stop of its own accord, either with a success or failure exit code. [Using indefinite one shot startup strategy](../examples/junit4/generic/src/test/java/org/testcontainers/containers/startupcheck/StartupCheckStrategyTest.java) inside_block:withIndefiniteOneShotStrategy ### Minimum duration startup strategy example Checks that the container is running and has been running for a defined minimum period of time. [Using minimum duration strategy](../examples/junit4/generic/src/test/java/org/testcontainers/containers/startupcheck/StartupCheckStrategyTest.java) inside_block:withMinimumDurationStrategy ### Other startup strategies If none of these options meet your requirements, you can create your own subclass of [`StartupCheckStrategy`](http://static.javadoc.io/org.testcontainers/testcontainers/{{ latest_version }}/org/testcontainers/containers /startupcheck/StartupCheckStrategy.html) with an appropriate startup check mechanism in `waitUntilStartupSuccessful()`. Or you can leave it as is and just implement the `checkStartupState(DockerClient dockerClient, String containerId)` if you still want to check state periodically. ## Depending on another container Sometimes, a container relies on another container to be ready before it should start itself. An example of this might be a database that needs to be started before your application container can link to it. You can tell a container that it depends on another container by using the `dependsOn` method: [Depending on another container](../examples/junit4/generic/src/test/java/generic/DependsOnTest.java) inside_block:dependsOn ================================================ FILE: docs/getting_help.md ================================================ # Getting help We hope that you find Testcontainers intuitive to use and reliable. However, sometimes things don't go the way we'd expect, and we'd like to try and help out if we can. To contact the Testcontainers team and other users you can: * Join our [Slack team](https://slack.testcontainers.org) * [Search our issues tracker](https://github.com/testcontainers/testcontainers-java/issues), or raise a new issue if you find any bugs or have suggested improvements * [Search Stack Overflow](https://stackoverflow.com/questions/tagged/testcontainers), especially among posts tagged with `testcontainers` ================================================ FILE: docs/index.md ================================================ # Testcontainers for Java

Not using Java? Here are other supported languages!

## About Testcontainers for Java *Testcontainers for Java* is a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container. Testcontainers make the following kinds of tests easier: * **Data access layer integration tests**: use a containerized instance of a MySQL, PostgreSQL or Oracle database to test your data access layer code for complete compatibility, but without requiring complex setup on developers' machines and safe in the knowledge that your tests will always start with a known DB state. Any other database type that can be containerized can also be used. * **Application integration tests**: for running your application in a short-lived test mode with dependencies, such as databases, message queues or web servers. * **UI/Acceptance tests**: use [containerized web browsers](modules/webdriver_containers.md), compatible with Selenium, for conducting automated UI tests. Each test can get a fresh instance of the browser, with no browser state, plugin variations or automated browser upgrades to worry about. And you get a video recording of each test session, or just each session where tests failed. * **Much more!** Check out the various contributed modules or create your own custom container classes using [`GenericContainer`](features/creating_container.md) as a base. ## Prerequisites * Docker - please see [General Docker requirements](supported_docker_environment/index.md) * A supported JVM testing framework: * [JUnit 4](test_framework_integration/junit_4.md) - See the [JUnit 4 Quickstart Guide](quickstart/junit_4_quickstart.md) * [Jupiter/JUnit 5](test_framework_integration/junit_5.md) * [Spock](test_framework_integration/spock.md) * *Or* manually add code to control the container/test lifecycle (See [hints for this approach](test_framework_integration/junit_4.md#manually-controlling-container-lifecycle)) ## Maven dependencies Testcontainers is distributed as separate JARs with a common version number: * A core JAR file for core functionality, generic containers and docker-compose support * A separate JAR file for each of the specialised modules. Each module's documentation describes the Maven/Gradle dependency to add to your project's build. For the core library, the latest Maven/Gradle dependency is as follows: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers {{latest_version}} test ``` You can also [check the latest version available on Maven Central](https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.testcontainers%22). ### Managing versions for multiple Testcontainers dependencies To avoid specifying the version of each dependency, you can use a `BOM` or `Bill Of Materials`. Using Maven you can add the following to `dependencyManagement` section in your `pom.xml`: === "Maven" ```xml org.testcontainers testcontainers-bom {{latest_version}} pom import ``` and then use dependencies without specifying a version: === "Maven" ```xml org.testcontainers testcontainers-mysql test ``` Using Gradle 5.0 or higher, you can add the following to the `dependencies` section in your `build.gradle`: === "Gradle" ```groovy implementation platform('org.testcontainers:testcontainers-bom:{{latest_version}}') //import bom testImplementation('org.testcontainers:testcontainers-mysql') //no version specified ``` [JitPack](jitpack_dependencies.md) builds are available for pre-release versions. !!! warning "Shaded dependencies" Testcontainers depends on other libraries (like docker-java) for it to work. Some of them (JUnit, docker-java-{api,transport} and its transitive dependencies, JNA, visible-assertions and others) are part of our public API. But there are also "private", implementation detail dependencies (e.g., docker-java-core, Guava, OkHttp, etc.) that are not exposed to public API but prone to conflicts with test code/application under test code. As such, **these libraries are 'shaded' into the core Testcontainers JAR** and relocated under `org.testcontainers.shaded` to prevent class conflicts. ## Sponsors A huge thank you to our sponsors: ### Bronze sponsors ### Donors ### Backers * [Philip Riecks (@rieckpil)](https://github.com/rieckpil) * [Karl Heinz Marbaise (@khmarbaise)](https://github.com/khmarbaise) * [Sascha Frinken (@sascha-frinken)](https://github.com/sascha-frinken) * [Christoph Dreis (@dreis2211)](https://github.com/dreis2211) * [Nikita Zhevnitskiy (@zhenik)](https://github.com/zhenik) * [Bas Stoker (@bastoker)](https://github.com/bastoker) * [Oleg Nenashev (@oleg-nenashev)](https://github.com/oleg-nenashev) * [Rik Glover (@rikglover)](https://github.com/rikglover) * [Amitosh Swain Mahapatra (@recrsn)](https://github.com/recrsn) * [Paris Apostolopoulos](https://opencollective.com/paris-apostolopoulos) ## Who is using Testcontainers? * [ZeroTurnaround](https://zeroturnaround.com) - Testing of the Java Agents, micro-services, Selenium browser automation * [Zipkin](https://zipkin.io) - MySQL and Cassandra testing * [Apache Gora](https://gora.apache.org) - CouchDB testing * [Apache James](https://james.apache.org) - LDAP and Cassandra integration testing * [StreamSets](https://github.com/streamsets/datacollector) - LDAP, MySQL Vault, MongoDB, Redis integration testing * [Playtika](https://github.com/Playtika/testcontainers-spring-boot) - Kafka, Couchbase, MariaDB, Redis, Neo4j, Aerospike, MemSQL * [JetBrains](https://www.jetbrains.com/) - Testing of the TeamCity plugin for HashiCorp Vault * [Plumbr](https://plumbr.io) - Integration testing of data processing pipeline micro-services * [Streamlio](https://streaml.io/) - Integration and Chaos Testing of our fast data platform based on Apache Pulsar, Apache BookKeeper and Apache Heron. * [Spring Session](https://projects.spring.io/spring-session/) - Redis, PostgreSQL, MySQL and MariaDB integration testing * [Apache Camel](https://camel.apache.org) - Testing Camel against native services such as Consul, Etcd and so on * [Infinispan](https://infinispan.org) - Testing the Infinispan Server as well as integration tests with databases, LDAP and KeyCloak * [Instana](https://www.instana.com) - Testing agents and stream processing backends * [eBay Marketing](https://www.ebay.com) - Testing for MySQL, Cassandra, Redis, Couchbase, Kafka, etc. * [Skyscanner](https://www.skyscanner.net/) - Integration testing against HTTP service mocks and various data stores * [Neo4j-OGM](https://neo4j.com/developer/neo4j-ogm/) - Testing with Neo4j * [Spring Data Neo4j](https://github.com/spring-projects/spring-data-neo4j/) - Testing imperative and reactive implementations with Neo4j * [Lightbend](https://www.lightbend.com/) - Testing [Alpakka Kafka](https://doc.akka.io/docs/alpakka-kafka/current/) and support in [Alpakka Kafka Testkit](https://doc.akka.io/docs/alpakka-kafka/current/testing.html#testing-with-kafka-in-docker) * [Zalando SE](https://corporate.zalando.com/en) - Testing core business services * [Europace AG](https://tech.europace.de/) - Integration testing for databases and micro services * [Micronaut Data](https://github.com/micronaut-projects/micronaut-data/) - Testing of Micronaut Data JDBC, a database access toolkit * [Vert.x SQL Client](https://github.com/eclipse-vertx/vertx-sql-client) - Testing with PostgreSQL, MySQL, MariaDB, SQL Server, etc. * [JHipster](https://www.jhipster.tech/) - Couchbase and Cassandra integration testing * [wescale](https://www.wescale.com) - Integration testing against HTTP service mocks and various data stores * [Marquez](https://marquezproject.github.io/marquez) - PostgreSQL integration testing * [Wise (formerly TransferWise)](https://wise.com) - Integration testing for different RDBMS, kafka and micro services * [XWiki](https://xwiki.org) - [Testing XWiki](https://dev.xwiki.org/xwiki/bin/view/Community/Testing/DockerTesting/) under all [supported configurations](https://dev.xwiki.org/xwiki/bin/view/Community/SupportStrategy/) * [Apache SkyWalking](http://github.com/apache/skywalking) - End-to-end testing of the Apache SkyWalking, and plugin tests of its subproject, [Apache SkyWalking Python](http://github.com/apache/skywalking-python), and of its eco-system built by the community, like [SkyAPM NodeJS Agent](http://github.com/SkyAPM/nodejs) * [jOOQ](https://www.jooq.org) - Integration testing all of jOOQ with a variety of RDBMS * [Trino (formerly Presto SQL)](https://trino.io) - Integration testing all Trino core & connectors, including tests of multi-node deployments and security configurations. * Google - Various open source projects: [OpenTelemetry](https://github.com/GoogleCloudPlatform/opentelemetry-operations-java), [Universal Application Tool](https://github.com/seattle-uat/universal-application-tool), [CloudBowl](https://github.com/GoogleCloudPlatform/cloudbowl-microservice-game) * [Backbase](https://www.backbase.com/) - Unit, Integration and Acceptance testing for different the databases supported (Oracle, SQL Server, MySQL), the different messaging systems supported (Kafka, Rabbit, AMQ) and other microservices and HTTP mocks. * [CloudBees](https://www.cloudbees.com/) - Integration testing of products, including but not limited to database and AWS/Localstack integration testing. * [Jenkins](https://www.jenkins.io/) - Integration testing of multiple plugins and the Trilead SSH2 fork maintained by the Jenkins community ([query](https://github.com/search?l=Maven+POM&q=org%3Ajenkinsci+testcontainers&type=Code)). * [Elastic](https://www.elastic.co) - Integration testing of the Java APM agent * [Alkira](https://www.alkira.com/) - Testing of multiple micro-services using Kafka, PostgreSQL, Apache Zookeeper, Etcd and so on. * [Togglz](https://www.togglz.org/) - Feature Flags for the Java platform * [Byzer](https://www.byzer.org/home) - Integration tests for Data and AI platforms are based on multiple versions of Byzer, Ray and Apache Spark. * [Apache SeaTunnel](https://github.com/apache/incubator-seatunnel) - Integration testing with different datasource. * [Bucket4j](https://github.com/bucket4j/bucket4j) - Java rate-limiting library based on the token-bucket algorithm. * [Spark ClickHouse Connector](https://github.com/housepower/spark-clickhouse-connector) - Integration tests for Apache Spark with both single node ClickHouse instance and multi-node ClickHouse cluster. * [Quarkus](https://github.com/quarkusio/quarkus) - Testcontainers is used extensively for Quarkus' [DevServices](https://quarkus.io/guides/dev-services) feature. * [Apache Kyuubi](https://kyuubi.apache.org) - Integration testing with Trino as data source engine, Kafka, etc. * [Dash0](https://www.dash0.com) - Integration testing for OpenTelemetry Observability product. ## License See [LICENSE](https://raw.githubusercontent.com/testcontainers/testcontainers-java/main/LICENSE). ## Attributions This project includes a modified class (ScriptUtils) taken from the Spring JDBC project, adapted under the terms of the Apache license. Copyright for that class remains with the original authors. This project was initially inspired by a [gist](https://gist.github.com/mosheeshel/c427b43c36b256731a0b) by [Moshe Eshel](https://github.com/mosheeshel). ## Copyright Copyright (c) 2015-2021 Richard North and other authors. See [AUTHORS](https://raw.githubusercontent.com/testcontainers/testcontainers-java/main/AUTHORS) for contributors. ================================================ FILE: docs/jitpack_dependencies.md ================================================ # JitPack (unreleased versions) If you like to live on the bleeding edge, [jitpack.io](https://jitpack.io) can be used to obtain SNAPSHOT versions. Use the following dependency description instead: === "Gradle" ```groovy testImplementation "com.github.testcontainers.testcontainers-java:--artifact name--:main-SNAPSHOT" ``` === "Maven" ```xml com.github.testcontainers.testcontainers-java --artifact name-- main-SNAPSHOT ``` A specific git revision (such as `02782d9`) can be used as a fixed version instead: === "Gradle" ```groovy testImplementation "com.github.testcontainers.testcontainers-java:--artifact name--:02782d9" ``` === "Maven" ```xml com.github.testcontainers.testcontainers-java --artifact name-- 02782d9 ``` The JitPack maven repository must also be declared, e.g.: === "Gradle" ```groovy repositories { maven { url "https://jitpack.io" } } ``` === "Maven" ```xml jitpack.io https://jitpack.io ``` ================================================ FILE: docs/js/tc-header.js ================================================ const mobileToggle = document.getElementById("mobile-menu-toggle"); const mobileSubToggle = document.getElementById("mobile-submenu-toggle"); function toggleMobileMenu() { document.body.classList.toggle('mobile-menu'); document.body.classList.toggle("mobile-tc-header-active"); } function toggleMobileSubmenu() { document.body.classList.toggle('mobile-submenu'); } if (mobileToggle) mobileToggle.addEventListener("click", toggleMobileMenu); if (mobileSubToggle) mobileSubToggle.addEventListener("click", toggleMobileSubmenu); const allParentMenuItems = document.querySelectorAll("#site-header .menu-item.has-children"); function clearActiveMenuItem() { document.body.classList.remove("tc-header-active"); allParentMenuItems.forEach((item) => { item.classList.remove("active"); }); } function setActiveMenuItem(e) { clearActiveMenuItem(); e.currentTarget.closest(".menu-item").classList.add("active"); document.body.classList.add("tc-header-active"); } allParentMenuItems.forEach((item) => { const trigger = item.querySelector(":scope > a, :scope > button"); trigger.addEventListener("click", (e) => { if (e.currentTarget.closest(".menu-item").classList.contains("active")) { clearActiveMenuItem(); } else { setActiveMenuItem(e); } }); trigger.addEventListener("mouseenter", (e) => { setActiveMenuItem(e); }); item.addEventListener("mouseleave", (e) => { clearActiveMenuItem(); }); }); ================================================ FILE: docs/modules/activemq.md ================================================ # ActiveMQ Testcontainers module for [ActiveMQ](https://hub.docker.com/r/apache/activemq-classic) and [Artemis](https://hub.docker.com/r/apache/activemq-artemis). ## ActiveMQContainer's usage examples You can start an ActiveMQ Classic container instance from any Java application by using: [Default ActiveMQ container](../../modules/activemq/src/test/java/org/testcontainers/activemq/ActiveMQContainerTest.java) inside_block:container With custom credentials: [Setting custom credentials](../../modules/activemq/src/test/java/org/testcontainers/activemq/ActiveMQContainerTest.java) inside_block:settingCredentials ## ArtemisContainer's usage examples You can start an ActiveMQ Artemis container instance from any Java application by using: [Default Artemis container](../../modules/activemq/src/test/java/org/testcontainers/activemq/ArtemisContainerTest.java) inside_block:container With custom credentials: [Setting custom credentials](../../modules/activemq/src/test/java/org/testcontainers/activemq/ArtemisContainerTest.java) inside_block:settingCredentials With anonymous login: [Allow anonymous login](../../modules/activemq/src/test/java/org/testcontainers/activemq/ArtemisContainerTest.java) inside_block:enableAnonymousLogin ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-activemq:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-activemq {{latest_version}} test ``` ================================================ FILE: docs/modules/azure.md ================================================ # Azure Module !!! note This module is INCUBATING. While it is ready for use and operational in the current version of Testcontainers, it is possible that it may receive breaking changes in the future. See [our contributing guidelines](/contributing/#incubating-modules) for more information on our incubating modules policy. Testcontainers module for the Microsoft Azure's [SDK](https://github.com/Azure/azure-sdk-for-java). Currently, the module supports `Azurite`, `Azure Event Hubs`, `Azure Service Bus` and `CosmosDB` emulators. In order to use them, you should use the following classes: Class | Container Image -|- AzuriteContainer | [mcr.microsoft.com/azure-storage/azurite](https://github.com/microsoft/containerregistry) EventHubsEmulatorContainer | [mcr.microsoft.com/azure-messaging/eventhubs-emulator](https://github.com/microsoft/containerregistry) ServiceBusEmulatorContainer | [mcr.microsoft.com/azure-messaging/servicebus-emulator](https://github.com/microsoft/containerregistry) CosmosDBEmulatorContainer | [mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator](https://github.com/microsoft/containerregistry) ## Usage example ### Azurite Storage Emulator Start Azurite Emulator during a test: [Starting an Azurite container](../../modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerTest.java) inside_block:emulatorContainer !!! note SSL configuration is possible using the `withSsl(MountableFile, String)` and `withSsl(MountableFile, MountableFile)` methods. If the tested application needs to use more than one set of credentials, the container can be configured to use custom credentials. Please see some examples below. [Starting an Azurite Blob container with one account and two keys](../../modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerTest.java) inside_block:withTwoAccountKeys [Starting an Azurite Blob container with more accounts and keys](../../modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerTest.java) inside_block:withMoreAccounts #### Using with Blob Build Azure Blob client: [Build Azure Blob Service client](../../modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerTest.java) inside_block:createBlobClient In case the application needs to use custom credentials, we can obtain them with a different method: [Obtain connection string with non-default credentials](../../modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerTest.java) inside_block:useNonDefaultCredentials #### Using with Queue Build Azure Queue client: [Build Azure Queue Service client](../../modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerTest.java) inside_block:createQueueClient !!! note We can use custom credentials the same way as defined in the Blob section. #### Using with Table Build Azure Table client: [Build Azure Table Service client](../../modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerTest.java) inside_block:createTableClient !!! note We can use custom credentials the same way as defined in the Blob section. ### Azure Event Hubs Emulator [Configuring the Azure Event Hubs Emulator container](../../modules/azure/src/test/resources/eventhubs_config.json) Start Azure Event Hubs Emulator during a test: [Setting up a network](../../modules/azure/src/test/java/org/testcontainers/azure/EventHubsEmulatorContainerTest.java) inside_block:network [Starting an Azurite container as dependency](../../modules/azure/src/test/java/org/testcontainers/azure/EventHubsEmulatorContainerTest.java) inside_block:azuriteContainer [Starting an Azure Event Hubs Emulator container](../../modules/azure/src/test/java/org/testcontainers/azure/EventHubsEmulatorContainerTest.java) inside_block:emulatorContainer #### Using Azure Event Hubs clients Configure the consumer and the producer clients: [Configuring the clients](../../modules/azure/src/test/java/org/testcontainers/azure/EventHubsEmulatorContainerTest.java) inside_block:createProducerAndConsumer ### Azure Service Bus Emulator [Configuring the Azure Service Bus Emulator container](../../modules/azure/src/test/resources/service-bus-config.json) Start Azure Service Bus Emulator during a test: [Setting up a network](../../modules/azure/src/test/java/org/testcontainers/azure/ServiceBusEmulatorContainerTest.java) inside_block:network [Starting a SQL Server container as dependency](../../modules/azure/src/test/java/org/testcontainers/azure/ServiceBusEmulatorContainerTest.java) inside_block:sqlContainer [Starting a Service Bus Emulator container](../../modules/azure/src/test/java/org/testcontainers/azure/ServiceBusEmulatorContainerTest.java) inside_block:emulatorContainer #### Using Azure Service Bus clients Configure the sender and the processor clients: [Configuring the sender client](../../modules/azure/src/test/java/org/testcontainers/azure/ServiceBusEmulatorContainerTest.java) inside_block:senderClient [Configuring the processor client](../../modules/azure/src/test/java/org/testcontainers/azure/ServiceBusEmulatorContainerTest.java) inside_block:processorClient ### CosmosDB Start Azure CosmosDB Emulator during a test: [Starting an Azure CosmosDB Emulator container](../../modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java) inside_block:emulatorContainer Prepare KeyStore to use for SSL. [Building KeyStore from certificate within container](../../modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java) inside_block:buildAndSaveNewKeyStore Set system trust-store parameters to use already built KeyStore: [Set system trust-store parameters](../../modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java) inside_block:setSystemTrustStoreParameters Build Azure CosmosDB client: [Build Azure CosmosDB client](../../modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java) inside_block:buildClient Test against the Emulator: [Testing against Azure CosmosDB Emulator container](../../modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java) inside_block:testWithClientAgainstEmulatorContainer ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-azure:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-azure {{latest_version}} test ``` ================================================ FILE: docs/modules/chromadb.md ================================================ # ChromaDB Testcontainers module for [ChromaDB](https://registry.hub.docker.com/r/chromadb/chroma) ## ChromaDB's usage examples You can start a ChromaDB container instance from any Java application by using: [Default ChromaDB container](../../modules/chromadb/src/test/java/org/testcontainers/chromadb/ChromaDBContainerTest.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:testcontainers-chromadb:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-chromadb {{latest_version}} test ``` ================================================ FILE: docs/modules/consul.md ================================================ # Hashicorp Consul Module Testcontainers module for [Consul](https://github.com/hashicorp/consul). Consul is a tool for managing key value properties. More information on Consul [here](https://www.consul.io/). ## Usage example [Running Consul in your Junit tests](../../modules/consul/src/test/java/org/testcontainers/consul/ConsulContainerTest.java) ## Why Consul in Junit tests? With the increasing popularity of Consul and config externalization, applications are now needing to source properties from Consul. This can prove challenging in the development phase without a running Consul instance readily on hand. This library aims to solve your apps integration testing with Consul. You can also use it to test how your application behaves with Consul by writing different test scenarios in Junit. ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-consul:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-consul {{latest_version}} test ``` ================================================ FILE: docs/modules/databases/cassandra.md ================================================ # Cassandra Module ## Usage example This example connects to the Cassandra cluster: 1. Define a container: [Container definition](../../../modules/cassandra/src/test/java/org/testcontainers/cassandra/CassandraContainerTest.java) inside_block:container-definition 2. Build a `CqlSession`: [Building CqlSession](../../../modules/cassandra/src/test/java/org/testcontainers/cassandra/CassandraContainerTest.java) inside_block:cql-session 3. Define a container with custom `cassandra.yaml` located in a directory `cassandra-auth-required-configuration`: [Running init script with required authentication](../../../modules/cassandra/src/test/java/org/testcontainers/cassandra/CassandraContainerTest.java) inside_block:init-with-auth ## Using secure connection (TLS) If you override the default `cassandra.yaml` with a version setting the property `client_encryption_options.optional` to `false`, you have to provide a valid client certificate and key (PEM format) when you initialize your container: [SSL setup](../../../modules/cassandra/src/test/java/org/testcontainers/cassandra/CassandraContainerTest.java) inside_block:with-ssl-config !!! hint To generate the client certificate and key, please refer to [this documentation](https://docs.datastax.com/en/cassandra-oss/3.x/cassandra/configuration/secureSSLCertificates.html). ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-cassandra:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-cassandra {{latest_version}} test ``` ================================================ FILE: docs/modules/databases/clickhouse.md ================================================ # Clickhouse Module Testcontainers module for [ClickHouse](https://hub.docker.com/r/clickhouse/clickhouse-server) ## Usage example You can start a ClickHouse container instance from any Java application by using: [Container definition](../../../modules/clickhouse/src/test/java/org/testcontainers/clickhouse/ClickHouseContainerTest.java) inside_block:container ### Testcontainers JDBC URL `jdbc:tc:clickhouse:18.10.3:///databasename` See [JDBC](./jdbc.md) for documentation. ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-clickhouse:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-clickhouse {{latest_version}} test ``` !!! hint Adding this Testcontainers library JAR will not automatically add a database driver JAR to your project. You should ensure that your project also has a suitable database driver as a dependency. ================================================ FILE: docs/modules/databases/cockroachdb.md ================================================ # CockroachDB Module Testcontainers module for [CockroachDB](https://hub.docker.com/r/cockroachdb/cockroach) ## Usage example You can start a CockroachDB container instance from any Java application by using: [Container definition](../../../modules/cockroachdb/src/test/java/org/testcontainers/cockroachdb/CockroachContainerTest.java) inside_block:container See [Database containers](./index.md) for documentation and usage that is common to all relational database container types. ### Testcontainers JDBC URL `jdbc:tc:cockroach:v21.2.3:///databasename` See [JDBC](./jdbc.md) for documentation. ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-cockroachdb:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-cockroachdb {{latest_version}} test ``` !!! hint Adding this Testcontainers library JAR will not automatically add a database driver JAR to your project. You should ensure that your project also has a suitable database driver as a dependency. ================================================ FILE: docs/modules/databases/couchbase.md ================================================ # Couchbase Module Testcontainers module for Couchbase. [Couchbase](https://www.couchbase.com/) is a document oriented NoSQL database. ## Usage example Running Couchbase as a stand-in in a test: 1. Define a bucket: [Bucket Definition](../../../modules/couchbase/src/test/java/org/testcontainers/couchbase/CouchbaseContainerTest.java) inside_block:bucket_definition 2. define a container: [Container definition](../../../modules/couchbase/src/test/java/org/testcontainers/couchbase/CouchbaseContainerTest.java) inside_block:container_definition 3. create an cluster: [Cluster creation](../../../modules/couchbase/src/test/java/org/testcontainers/couchbase/CouchbaseContainerTest.java) inside_block:cluster_creation ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-couchbase:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-couchbase {{latest_version}} test ``` ================================================ FILE: docs/modules/databases/cratedb.md ================================================ # CrateDB Module Testcontainers module for [CrateDB](https://hub.docker.com/_/crate) ## Usage example You can start a CrateDB container instance from any Java application by using: [Container definition](../../../modules/cratedb/src/test/java/org/testcontainers/junit/cratedb/SimpleCrateDBTest.java) inside_block:container See [Database containers](./index.md) for documentation and usage that is common to all relational database container types. ### Testcontainers JDBC URL `jdbc:tc:cratedb:5.2.3:///databasename` See [JDBC](./jdbc.md) for documentation. ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-cratedb:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-cratedb {{latest_version}} test ``` !!! hint Adding this Testcontainers library JAR will not automatically add a database driver JAR to your project. You should ensure that your project also has a suitable database driver as a dependency. ================================================ FILE: docs/modules/databases/databend.md ================================================ # Databend Module Testcontainers module for [Databend](https://hub.docker.com/r/datafuselabs/databend) ## Usage example You can start a Databend container instance from any Java application by using: [Container definition](../../../modules/databend/src/test/java/org/testcontainers/databend/DatabendContainerTest.java) inside_block:container ### Testcontainers JDBC URL `jdbc:tc:databend:v1.2.615:///databasename` See [JDBC](./jdbc.md) for documentation. ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-databend:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-databend {{latest_version}} test ``` !!! hint Adding this Testcontainers library JAR will not automatically add a database driver JAR to your project. You should ensure that your project also has a suitable database driver as a dependency. ================================================ FILE: docs/modules/databases/db2.md ================================================ # DB2 Module Testcontainers module for [DB2](https://www.ibm.com/docs/en/db2/11.5.x?topic=deployments-db2-community-edition-docker) ## Usage example You can start a DB2 container instance from any Java application by using: [Container definition](../../../modules/db2/src/test/java/org/testcontainers/db2/Db2ContainerTest.java) inside_block:container !!! warning "EULA Acceptance" Due to licencing restrictions you are required to accept an EULA for this container image. To indicate that you accept the DB2 image EULA, call the `acceptLicense()` method, or place a file at the root of the classpath named `container-license-acceptance.txt`, e.g. at `src/test/resources/container-license-acceptance.txt`. This file should contain the line: `ibmcom/db2:11.5.0.0a` (or, if you are overriding the docker image name/tag, update accordingly). See [Database containers](./index.md) for documentation and usage that is common to all relational database container types. ### Testcontainers JDBC URL `jdbc:tc:db2:11.5.0.0a:///databasename` See [JDBC](./jdbc.md) for documentation. ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-db2:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-db2 {{latest_version}} test ``` !!! hint Adding this Testcontainers library JAR will not automatically add a database driver JAR to your project. You should ensure that your project also has a suitable database driver as a dependency. ================================================ FILE: docs/modules/databases/index.md ================================================ # Database containers ## Overview You might want to use Testcontainers' database support: * **Instead of H2 database for DAO unit tests that depend on database features that H2 doesn't emulate.** Testcontainers is not as performant as H2, but does give you the benefit of 100% database compatibility (since it runs a real DB inside of a container). * **Instead of a database running on the local machine or in a VM** for DAO unit tests or end-to-end integration tests that need a database to be present. In this context, the benefit of Testcontainers is that the database always starts in a known state, without any contamination between test runs or on developers' local machines. !!! note Of course, it's still important to have as few tests that hit the database as possible, and make good use of mocks for components higher up the stack. See [JDBC](./jdbc.md) and [R2DBC](./r2dbc.md) for information on how to use Testcontainers with SQL-like databases. ================================================ FILE: docs/modules/databases/influxdb.md ================================================ # InfluxDB Module Testcontainers module for InfluxData [InfluxDB](https://www.influxdata.com/products/influxdb/). ## Important note There are breaking changes in InfluxDB 2.x. For more information refer to the main [documentation](https://docs.influxdata.com/influxdb/v2.0/upgrade/v1-to-v2/). You can find more information about the official InfluxDB image on [Docker Hub](https://hub.docker.com/_/influxdb). ## InfluxDB 2.x usage example Running a `InfluxDBContainer` as a stand-in for InfluxDB in a test: [Create an InfluxDB container](../../../modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerTest.java) inside_block:constructorWithDefaultVariables The InfluxDB instance will be setup with the following data:
| Property | Default Value | |--------------|:-------------:| | username | test-user | | password | test-password | | organization | test-org | | bucket | test-bucket | | retention | 0 (infinite) | | adminToken | - | For more details about the InfluxDB setup, please visit the official [InfluxDB documentation](https://docs.influxdata.com/influxdb/v2.0/upgrade/v1-to-v2/docker/#influxdb-2x-initialization-credentials). It is possible to overwrite the default property values. Create a container with InfluxDB admin token: [Create an InfluxDB container with admin token](../../../modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerTest.java) inside_block:constructorWithAdminToken Or create a container with custom username, password, bucket, organization, and retention time: [Create an InfluxDB container with custom settings](../../../modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerTest.java) inside_block:constructorWithCustomVariables The following code snippet shows how you can create an InfluxDB Java client: [Create an InfluxDB Java client](../../../modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerTest.java) inside_block:createInfluxDBClient !!! hint You can find the latest documentation about the InfluxDB 2.x Java client [here](https://github.com/influxdata/influxdb-client-java). ## InfluxDB 1.x usage example Running a `InfluxDBContainer` as a stand-in for InfluxDB in a test with default env variables: [Create an InfluxDB container](../../../modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerV1Test.java) inside_block:constructorWithDefaultVariables The InfluxDB instance will be setup with the following data:
| Property | Default Value | |---------------|:-------------:| | username | test-user | | password | test-password | | authEnabled | true | | admin | admin | | adminPassword | password | | database | - | It is possible to overwrite the default values. For instance, creating an InfluxDB container with a custom username, password, and database name: [Create an InfluxDB container with custom settings](../../../modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerV1Test.java) inside_block:constructorWithUserPassword In the following example you will find a snippet to create an InfluxDB client using the official Java client: [Create an InfluxDB Java client](../../../modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerV1Test.java) inside_block:createInfluxDBClient !!! hint You can find the latest documentation about the InfluxDB 1.x Java client [here](https://github.com/influxdata/influxdb-java). ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-influxdb:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-influxdb {{latest_version}} test ``` !!! hint Adding this Testcontainers library JAR will not automatically add a database driver JAR to your project. You should ensure that your project also has a suitable database driver as a dependency. ================================================ FILE: docs/modules/databases/jdbc.md ================================================ # JDBC support You can obtain a temporary database in one of two ways: * **Using a specially modified JDBC URL**: after making a very simple modification to your system's JDBC URL string, Testcontainers will provide a disposable stand-in database that can be used without requiring modification to your application code. * **JUnit @Rule/@ClassRule**: this mode starts a database inside a container before your tests and tears it down afterwards. ## Database containers launched via JDBC URL scheme As long as you have Testcontainers and the appropriate JDBC driver on your classpath, you can simply modify regular JDBC connection URLs to get a fresh containerized instance of the database each time your application starts up. _N.B:_ * _TC needs to be on your application's classpath at runtime for this to work_ * _For Spring Boot (Before version `2.3.0`) you need to specify the driver manually `spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver`_ **Original URL**: `jdbc:mysql://localhost:3306/databasename` Insert `tc:` after `jdbc:` as follows. Note that the hostname, port and database name will be ignored; you can leave these as-is or set them to any value. !!! note We will use `///` (host-less URIs) from now on to emphasis the unimportance of the `host:port` pair. From Testcontainers' perspective, `jdbc:mysql:8.0.36://localhost:3306/databasename` and `jdbc:mysql:8.0.36:///databasename` is the same URI. !!! warning If you're using the JDBC URL support, there is no need to instantiate an instance of the container - Testcontainers will do it automagically. ### JDBC URL examples #### Using ClickHouse `jdbc:tc:clickhouse:18.10.3:///databasename` #### Using CockroachDB `jdbc:tc:cockroach:v21.2.3:///databasename` #### Using CrateDB `jdbc:tc:cratedb:5.2.3:///databasename` #### Using DB2 `jdbc:tc:db2:11.5.0.0a:///databasename` #### Using MariaDB `jdbc:tc:mariadb:10.3.39:///databasename` #### Using MySQL `jdbc:tc:mysql:8.0.36:///databasename` #### Using MSSQL Server `jdbc:tc:sqlserver:2017-CU12:///databasename` #### Using OceanBase `jdbc:tc:oceanbasece:4.2.1-lts:///databasename` #### Using Oracle `jdbc:tc:oracle:21-slim-faststart:///databasename` #### Using PostGIS `jdbc:tc:postgis:9.6-2.5:///databasename` #### Using PostgreSQL `jdbc:tc:postgresql:9.6.8:///databasename` #### Using QuestDB `jdbc:tc:questdb:6.5.3:///databasename` #### Using TimescaleDB `jdbc:tc:timescaledb:2.1.0-pg13:///databasename` #### Using PGVector `jdbc:tc:pgvector:pg16:///databasename` #### Using TiDB `jdbc:tc:tidb:v6.1.0:///databasename` #### Using Timeplus `jdbc:tc:timeplus:2.3.21:///databasename` #### Using Trino `jdbc:tc:trino:352://localhost/memory/default` #### Using YugabyteDB `jdbc:tc:yugabyte:2.14.4.0-b26:///databasename` ### Using a classpath init script Testcontainers can run an init script after the database container is started, but before your code is given a connection to it. The script must be on the classpath, and is referenced as follows: `jdbc:tc:mysql:8.0.36:///databasename?TC_INITSCRIPT=somepath/init_mysql.sql` This is useful if you have a fixed script for setting up database schema, etc. ### Using an init script from a file If the init script path is prefixed `file:`, it will be loaded from a file (relative to the working directory, which will usually be the project root). `jdbc:tc:mysql:8.0.36:///databasename?TC_INITSCRIPT=file:src/main/resources/init_mysql.sql` ### Using an init function Instead of running a fixed script for DB setup, it may be useful to call a Java function that you define. This is intended to allow you to trigger database schema migration tools. To do this, add TC_INITFUNCTION to the URL as follows, passing a full path to the class name and method: `jdbc:tc:mysql:8.0.36:///databasename?TC_INITFUNCTION=org.testcontainers.jdbc.JDBCDriverTest::sampleInitFunction` The init function must be a public static method which takes a `java.sql.Connection` as its only parameter, e.g. ```java public class JDBCDriverTest { public static void sampleInitFunction(Connection connection) throws SQLException { // e.g. run schema setup or Flyway/liquibase/etc DB migrations here... } ... ``` ### Running container in daemon mode By default database container is being stopped as soon as last connection is closed. There are cases when you might need to start container and keep it running till you stop it explicitly or JVM is shutdown. To do this, add `TC_DAEMON` parameter to the URL as follows: `jdbc:tc:mysql:8.0.36:///databasename?TC_DAEMON=true` With this parameter database container will keep running even when there's no open connections. ### Running container with tmpfs options Container can have `tmpfs` mounts for storing data in host memory. This is useful if you want to speed up your database tests. Be aware that the data will be lost when the container stops. To pass this option to the container, add `TC_TMPFS` parameter to the URL as follows: `jdbc:tc:postgresql:9.6.8:///databasename?TC_TMPFS=/testtmpfs:rw` If you need more than one option, separate them by comma (e.g. `TC_TMPFS=key:value,key1:value1&other_parameters=foo`). For more information about `tmpfs` mount, see [the official Docker documentation](https://docs.docker.com/storage/tmpfs/). ## Database container objects In case you can't use the URL support, or need to fine-tune the container, you can instantiate it yourself. Add a @Rule or @ClassRule to your test class, e.g.: ```java public class SimpleMySQLTest { @Rule public MySQLContainer mysql = new MySQLContainer(); ``` Now, in your test code (or a suitable setup method), you can obtain details necessary to connect to this database: * `mysql.getJdbcUrl()` provides a JDBC URL your code can connect to * `mysql.getUsername()` provides the username your code should pass to the driver * `mysql.getPassword()` provides the password your code should pass to the driver Note that if you use `@Rule`, you will be given an isolated container for each test method. If you use `@ClassRule`, you will get on isolated container for all the methods in the test class. Examples/Tests: * [MySQL](https://github.com/testcontainers/testcontainers-java/blob/main/modules/mysql/src/test/java/org/testcontainers/junit/mysql/SimpleMySQLTest.java) * [PostgreSQL](https://github.com/testcontainers/testcontainers-java/blob/main/modules/postgresql/src/test/java/org/testcontainers/junit/postgresql/SimplePostgreSQLTest.java) ================================================ FILE: docs/modules/databases/mariadb.md ================================================ # MariaDB Module Testcontainers module for [MariaDB](https://hub.docker.com/_/mariadb) ## Usage example You can start a MySQL container instance from any Java application by using: [Container definition](../../../modules/mariadb/src/test/java/org/testcontainers/mariadb/MariaDBContainerTest.java) inside_block:container See [Database containers](./index.md) for documentation and usage that is common to all relational database container types. ### Testcontainers JDBC URL `jdbc:tc:mariadb:10.3.39:///databasename` See [JDBC](./jdbc.md) for documentation. ## MariaDB `root` user password If no custom password is specified, the container will use the default user password `test` for the `root` user as well. When you specify a custom password for the database user, this will also act as the password of the MariaDB `root` user automatically. ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-mariadb:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-mariadb {{latest_version}} test ``` !!! hint Adding this Testcontainers library JAR will not automatically add a database driver JAR to your project. You should ensure that your project also has a suitable database driver as a dependency. ================================================ FILE: docs/modules/databases/mongodb.md ================================================ # MongoDB Module The MongoDB module provides two Testcontainers for MongoDB unit testing: * [MongoDBContainer](#mongodbcontainer) - the core MongoDB database * [MongoDBAtlasLocalContainer](#mongodbatlaslocalcontainer) - the core MongoDB database combined with MongoDB Atlas Search + Atlas Vector Search ## MongoDBContainer ### Usage example The following example shows how to create a MongoDBContainer: [Creating a MongoDB container](../../../modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBContainerTest.java) inside_block:creatingMongoDBContainer And how to start it: [Starting a MongoDB container](../../../modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBContainerTest.java) inside_block:startingMongoDBContainer !!! note To construct a multi-node MongoDB cluster, consider the [mongodb-replica-set project](https://github.com/silaev/mongodb-replica-set/) #### Motivation Implement a reusable, cross-platform, simple to install solution that doesn't depend on fixed ports to test MongoDB transactions. #### General info MongoDB starting from version 4 supports multi-document transactions only for a replica set. For instance, to initialize a single node replica set on fixed ports via Docker, one has to do the following: * Run a MongoDB container of version 4 and up specifying --replSet command * Initialize a single replica set via executing a proper command * Wait for the initialization to complete * Provide a special url for a user to employ with a MongoDB driver without specifying replicaSet As we can see, there is a lot of operations to execute and we even haven't touched a non-fixed port approach. That's where the MongoDBContainer might come in handy. ## MongoDBAtlasLocalContainer ### Usage example The following example shows how to create a MongoDBAtlasLocalContainer: [Creating a MongoDB Atlas Local Container](../../../modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBAtlasLocalContainerTest.java) inside_block:creatingAtlasLocalContainer And how to start it: [Start the Container](../../../modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBAtlasLocalContainerTest.java) inside_block:startingAtlasLocalContainer The connection string provided by the MongoDBAtlasLocalContainer's getConnectionString() method includes the dynamically allocated port: [Get the Connection String](../../../modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBAtlasLocalContainerTest.java) inside_block:getConnectionStringAtlasLocalContainer e.g. `mongodb://localhost:12345/?directConnection=true` ### References MongoDB Atlas Local combines the MongoDB database engine with MongoT, a sidecar process for advanced searching capabilities built by MongoDB and powered by [Apache Lucene](https://lucene.apache.org/). The container (mongodb/mongodb-atlas-local) documentation can be found [here](https://www.mongodb.com/docs/atlas/cli/current/atlas-cli-deploy-docker/). General information about Atlas Search can be found [here](https://www.mongodb.com/docs/atlas/atlas-search/). ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-mongodb:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-mongodb {{latest_version}} test ``` !!! hint Adding this Testcontainers library JAR will not automatically add a database driver JAR to your project. You should ensure that your project also has a suitable database driver as a dependency #### Copyright Copyright (c) 2019 Konstantin Silaev ================================================ FILE: docs/modules/databases/mssqlserver.md ================================================ # MS SQL Server Module Testcontainers module for [MS SQL Server](https://mcr.microsoft.com/en-us/artifact/mar/mssql/server/) ## Usage example You can start a MS SQL Server container instance from any Java application by using: [Container definition](../../../modules/mssqlserver/src/test/java/org/testcontainers/mssqlserver/MSSQLServerContainerTest.java) inside_block:container !!! warning "EULA Acceptance" Due to licencing restrictions you are required to accept an EULA for this container image. To indicate that you accept the MS SQL Server image EULA, call the `acceptLicense()` method, or place a file at the root of the classpath named `container-license-acceptance.txt`, e.g. at `src/test/resources/container-license-acceptance.txt`. This file should contain the line: `mcr.microsoft.com/mssql/server:2017-CU12` (or, if you are overriding the docker image name/tag, update accordingly). Please see the [`microsoft-mssql-server` image documentation](https://hub.docker.com/_/microsoft-mssql-server#environment-variables) for a link to the EULA document. See [Database containers](./index.md) for documentation and usage that is common to all relational database container types. ### Testcontainers JDBC URL `jdbc:tc:sqlserver:2017-CU12:///databasename` See [JDBC](./jdbc.md) for documentation. ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-mssqlserver:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-mssqlserver {{latest_version}} test ``` !!! hint Adding this Testcontainers library JAR will not automatically add a database driver JAR to your project. You should ensure that your project also has a suitable database driver as a dependency. ## License See [LICENSE](https://raw.githubusercontent.com/testcontainers/testcontainers-java/main/modules/mssqlserver/LICENSE). ## Copyright Copyright (c) 2017 - 2019 G DATA Software AG and other authors. See [AUTHORS](https://raw.githubusercontent.com/testcontainers/testcontainers-java/main/modules/mssqlserver/AUTHORS) for contributors. ================================================ FILE: docs/modules/databases/mysql.md ================================================ # MySQL Module Testcontainers module for [MySQL](https://hub.docker.com/_/mysql) ## Usage example You can start a MySQL container instance from any Java application by using: [Container definition](../../../modules/mysql/src/test/java/org/testcontainers/mysql/MySQLContainerTest.java) inside_block:container See [Database containers](./index.md) for documentation and usage that is common to all relational database container types. ### Testcontainers JDBC URL `jdbc:tc:mysql:8.0.36:///databasename` See [JDBC](./jdbc.md) for documentation. ## Overriding MySQL my.cnf settings For MySQL databases, it is possible to override configuration settings using resources on the classpath. Assuming `somepath/mysql_conf_override` is a directory on the classpath containing .cnf files, the following URL can be used: `jdbc:tc:mysql:8.0.36://hostname/databasename?TC_MY_CNF=somepath/mysql_conf_override` Any .cnf files in this classpath directory will be mapped into the database container's /etc/mysql/conf.d directory, and will be able to override server settings when the container starts. ## MySQL `root` user password If no custom password is specified, the container will use the default user password `test` for the `root` user as well. When you specify a custom password for the database user, this will also act as the password of the MySQL `root` user automatically. ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-mysql:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-mysql {{latest_version}} test ``` !!! hint Adding this Testcontainers library JAR will not automatically add a database driver JAR to your project. You should ensure that your project also has a suitable database driver as a dependency. ================================================ FILE: docs/modules/databases/neo4j.md ================================================ # Neo4j Module This module helps to run [Neo4j](https://neo4j.com/download/) using Testcontainers. Note that it's based on the [official Docker image](https://hub.docker.com/_/neo4j/) provided by Neo4j, Inc. Even though the latest LTS version of Neo4j 4.4 is used in the examples of this documentation, the Testcontainers integration supports also newer 5.x images of Neo4j. ## Usage example Declare your Testcontainers as a `@ClassRule` or `@Rule` in a JUnit 4 test or as static or member attribute of a JUnit 5 test annotated with `@Container` as you would with other Testcontainers. You can either use call `getBoltUrl()` or `getHttpUrl()` on the Neo4j container. `getBoltUrl()` is meant to be used with one of the [official Bolt drivers](https://neo4j.com/developer/language-guides/) while `getHttpUrl()` gives you the HTTP-address of the transactional HTTP endpoint. On the JVM you would most likely use the [Java driver](https://github.com/neo4j/neo4j-java-driver). The following example uses the JUnit 5 extension `@Testcontainers` and demonstrates both the usage of the Java Driver and the REST endpoint: [JUnit 5 example](../../../examples/neo4j-container/src/test/java/org/testcontainers/containers/Neo4jExampleTest.java) inside_block:junitExample You are not limited to Unit tests, and you can use an instance of the Neo4j Testcontainers in vanilla Java code as well. ## Additional features ### Custom password A custom password can be provided: [Custom password](../../../modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java) inside_block:withAdminPassword ### Disable authentication Authentication can be disabled: [Disable authentication](../../../modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java) inside_block:withoutAuthentication ### Random password A random (`UUID`-random based) password can be set: [Random password](../../../modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java) inside_block:withRandomPassword ### Neo4j-Configuration Neo4j's Docker image needs Neo4j configuration options in a dedicated format. The container takes care of that, and you can configure the database with standard options like the following: [Neo4j configuration](../../../modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java) inside_block:neo4jConfiguration ### Add custom plugins Custom plugins, like APOC, can be copied over to the container from any classpath or host resource like this: [Plugin jar](../../../modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java) inside_block:registerPluginsJar Whole directories work as well: [Plugin folder](../../../modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java) inside_block:registerPluginsPath ### Add Neo4j Docker Labs plugins Add any Neo4j Labs plugin from the [Neo4j 4.4 Docker Labs plugin list](https://neo4j.com/docs/operations-manual/4.4/docker/operations/#docker-neo4jlabs-plugins) or [Neo4j 5 plugin list](https://neo4j.com/docs/operations-manual/5/configuration/plugins/). !!! note The methods `withLabsPlugins(Neo4jLabsPlugin...)` and `withLabsPlugins(String... plugins)` are deprecated. Please the method `withPlugins(String... plugins)`. [Configure Neo4j Labs Plugins](../../../modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java) inside_block:configureLabsPlugins ### Start the container with a predefined database If you have an existing database (`graph.db`) you want to work with, copy it over to the container like this: [Copy database](../../../modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java) inside_block:copyDatabase !!! note The `withDatabase` method will only work with Neo4j 3.5 and throw an exception if used in combination with a newer version. ## Choose your Neo4j license If you need the Neo4j enterprise license, you can declare your Neo4j container like this: [Enterprise edition](../../../modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java) inside_block:enterpriseEdition This creates a Testcontainers based on the Docker image build with the Enterprise version of Neo4j 4.4. The call to `withEnterpriseEdition` adds the required environment variable that you accepted the terms and condition of the enterprise version. You accept those by adding a file named `container-license-acceptance.txt` to the root of your classpath containing the text `neo4j:4.4-enterprise` in one line. If you are planning to run a newer Neo4j 5.x enterprise edition image, you have to manually define the proper enterprise image (e.g. `neo4j:5-enterprise`) and set the environment variable `NEO4J_ACCEPT_LICENSE_AGREEMENT` by adding `.withEnv("NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes")` to your container definition. You'll find more information about licensing Neo4j here: [About Neo4j Licenses](https://neo4j.com/licensing/). ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-neo4j:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-neo4j {{latest_version}} test ``` !!! hint Add the Neo4j Java driver if you plan to access the Testcontainers via Bolt: === "Gradle" ```groovy compile "org.neo4j.driver:neo4j-java-driver:4.4.13" ``` === "Maven" ```xml org.neo4j.driver neo4j-java-driver 4.4.13 ``` ================================================ FILE: docs/modules/databases/oceanbase.md ================================================ # OceanBase Module Testcontainers module for [OceanBase](https://hub.docker.com/r/oceanbase/oceanbase-ce) ## Usage example You can start an OceanBase container instance from any Java application by using: [Container definition](../../../modules/oceanbase/src/test/java/org/testcontainers/oceanbase/SimpleOceanBaseCETest.java) inside_block:container See [Database containers](./index.md) for documentation and usage that is common to all relational database container types. ### Testcontainers JDBC URL `jdbc:tc:oceanbasece:4.2.1-lts:///databasename` See [JDBC](./jdbc.md) for documentation. ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-oceanbase:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-oceanbase {{latest_version}} test ``` !!! hint Adding this Testcontainers library JAR will not automatically add a database driver JAR to your project. You should ensure that your project also has a suitable database driver as a dependency. ================================================ FILE: docs/modules/databases/oraclefree.md ================================================ # Oracle Database Free Module Testcontainers module for [Oracle Free](https://hub.docker.com/r/gvenzl/oracle-free) ## Usage example You can start an Oracle-Free container instance from any Java application by using: [Container creation](../../../modules/oracle-free/src/test/java/org/testcontainers/junit/oracle/SimpleOracleTest.java) inside_block:container See [Database containers](./index.md) for documentation and usage that is common to all relational database container types. ### Testcontainers JDBC URL `jdbc:tc:oracle:21-slim-faststart:///databasename` See [JDBC](./jdbc.md) for documentation. ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-oracle-free:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-oracle-free {{latest_version}} test ``` !!! hint Adding this Testcontainers library JAR will not automatically add a database driver JAR to your project. You should ensure that your project also has a suitable database driver as a dependency. ================================================ FILE: docs/modules/databases/oraclexe.md ================================================ # Oracle-XE Module Testcontainers module for [Oracle XE](https://hub.docker.com/r/gvenzl/oracle-xe) ## Usage example You can start an Oracle-XE container instance from any Java application by using: [Container creation](../../../modules/oracle-xe/src/test/java/org/testcontainers/junit/oracle/SimpleOracleTest.java) inside_block:container See [Database containers](./index.md) for documentation and usage that is common to all relational database container types. ### Testcontainers JDBC URL `jdbc:tc:oracle:21-slim-faststart:///databasename` See [JDBC](./jdbc.md) for documentation. ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-oracle-xe:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-oracle-xe {{latest_version}} test ``` !!! hint Adding this Testcontainers library JAR will not automatically add a database driver JAR to your project. You should ensure that your project also has a suitable database driver as a dependency. ================================================ FILE: docs/modules/databases/orientdb.md ================================================ # OrientDB Module Testcontainers module for [OrientDB](https://hub.docker.com/_/orientdb/) ## Usage example You can start an OrientDB container instance from any Java application by using: [Container creation](../../../modules/orientdb/src/test/java/org/testcontainers/orientdb/OrientDBContainerTest.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:testcontainers-orientdb:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-orientdb {{latest_version}} test ``` !!! hint Add the OrientDB Java client if you plan to access the Testcontainer: === "Gradle" ```groovy compile "com.orientechnologies:orientdb-client:3.0.24" ``` === "Maven" ```xml com.orientechnologies orientdb-client 3.0.24 ``` ================================================ FILE: docs/modules/databases/postgres.md ================================================ # Postgres Module Testcontainers module for [PostgresSQL](https://hub.docker.com/_/postgres) ## Usage example You can start a PostgreSQL container instance from any Java application by using: [Container creation](../../../modules/postgresql/src/test/java/org/testcontainers/postgresql/PostgreSQLContainerTest.java) inside_block:container See [Database containers](./index.md) for documentation and usage that is common to all relational database container types. ### Testcontainers JDBC URL * PostgreSQL: `jdbc:tc:postgresql:9.6.8:///databasename` * PostGIS: `jdbc:tc:postgis:9.6-2.5:///databasename` * TimescaleDB: `jdbc:tc:timescaledb:2.1.0-pg13:///databasename` * PGvector: `jdbc:tc:pgvector:pg16:///databasename` See [JDBC](./jdbc.md) for documentation. ## Compatible images `PostgreSQLContainer` can also be used with the following images: * [pgvector/pgvector](https://hub.docker.com/r/pgvector/pgvector) [Using pgvector](../../../modules/postgresql/src/test/java/org/testcontainers/postgresql/CompatibleImageTest.java) inside_block:pgvectorContainer * [postgis/postgis](https://registry.hub.docker.com/r/postgis/postgis) [Using PostGIS](../../../modules/postgresql/src/test/java/org/testcontainers/postgresql/CompatibleImageTest.java) inside_block:postgisContainer * [timescale/timescaledb](https://hub.docker.com/r/timescale/timescaledb) [Using TimescaleDB](../../../modules/postgresql/src/test/java/org/testcontainers/postgresql/CompatibleImageTest.java) inside_block:timescaledbContainer ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-postgresql:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-postgresql {{latest_version}} test ``` !!! hint Adding this Testcontainers library JAR will not automatically add a database driver JAR to your project. You should ensure that your project also has a suitable database driver as a dependency. ================================================ FILE: docs/modules/databases/presto.md ================================================ # Presto Module !!! note This module is deprecated, use Trino module. See [Database containers](./index.md) for documentation and usage that is common to all database container types. ## Usage example Running Presto as a stand-in for in a test: ```java public class SomeTest { @Rule public PrestoContainer presto = new PrestoContainer(); @Test public void someTestMethod() { String url = presto.getJdbcUrl(); ... create a connection and run test as normal ``` Presto comes with several catalogs preconfigured. Most useful ones for testing are * `tpch` catalog using the [Presto TPCH Connector](https://prestosql.io/docs/current/connector/tpch.html). This is a read-only catalog that defines standard TPCH schema, so is available for querying without a need to create any tables. * `memory` catalog using the [Presto Memory Connector](https://prestosql.io/docs/current/connector/memory.html). This catalog can be used for creating schemas and tables and does not require any storage, as everything is stored fully in-memory. Example test using the `tpch` and `memory` catalogs: ```java public class SomeTest { @Rule public PrestoContainer prestoSql = new PrestoContainer(); @Test public void queryMemoryAndTpchConnectors() throws SQLException { try (Connection connection = prestoSql.createConnection(); Statement statement = connection.createStatement()) { // Prepare data statement.execute("CREATE TABLE memory.default.table_with_array AS SELECT 1 id, ARRAY[1, 42, 2, 42, 4, 42] my_array"); // Query Presto using newly created table and a builtin connector try (ResultSet resultSet = statement.executeQuery("" + "SELECT nationkey, element " + "FROM tpch.tiny.nation " + "JOIN memory.default.table_with_array twa ON nationkey = twa.id " + "LEFT JOIN UNNEST(my_array) a(element) ON true " + "ORDER BY element OFFSET 1 FETCH NEXT 3 ROWS WITH TIES ")) { List actualElements = new ArrayList<>(); while (resultSet.next()) { actualElements.add(resultSet.getInt("element")); } Assert.assertEquals(Arrays.asList(2, 4, 42, 42, 42), actualElements); } } } } ``` ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-presto:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-presto {{latest_version}} test ``` !!! hint Adding this Testcontainers library JAR will not automatically add the Presto JDBC driver JAR to your project. You should ensure that your project has the Presto JDBC driver as a dependency, if you plan on using it. Refer to [Presto project download page](https://prestosql.io/download.html) for instructions. ================================================ FILE: docs/modules/databases/questdb.md ================================================ # QuestDB Module Testcontainers module for [QuestDB](https://hub.docker.com/r/questdb/questdb) ## Usage example You can start a QuestDB container instance from any Java application by using: [Container creation](../../../modules/questdb/src/test/java/org/testcontainers/junit/questdb/SimpleQuestDBTest.java) inside_block:container See [Database containers](./index.md) for documentation and usage that is common to all relational database container types. ### Testcontainers JDBC URL `jdbc:tc:questdb:6.5.3:///databasename` See [JDBC](./jdbc.md) for documentation. ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-questdb:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-questdb {{latest_version}} test ``` ================================================ FILE: docs/modules/databases/r2dbc.md ================================================ # R2DBC support You can obtain a temporary database in one of two ways: * **Using a specially modified R2DBC URL**: after making a very simple modification to your system's R2DBC URL string, Testcontainers will provide a disposable stand-in database that can be used without requiring modification to your application code. * **JUnit @Rule/@ClassRule**: this mode starts a database inside a container before your tests and tears it down afterwards. ## Database containers launched via R2DBC URL scheme As long as you have Testcontainers and the appropriate R2DBC driver on your classpath, you can simply modify regular R2DBC connection URLs to get a fresh containerized instance of the database each time your application starts up. The started container will be terminated when the `ConnectionFactory` is closed. !!! warning Both the database module (e.g. `org.testcontainers:testcontainers-mysql`) **and** `org.testcontainers:testcontainers-r2dbc` need to be on your application's classpath at runtime. **Original URL**: `r2dbc:mysql://localhost:3306/databasename` 1. Insert `tc:` after `r2dbc:` as follows. Note that the hostname, port and database name will be ignored; you can leave these as-is or set them to any value. 1. Specify the mandatory Docker tag of the database's official image that you want using a `TC_IMAGE_TAG` query parameter. **Note that, unlike Testcontainers' JDBC URL support, it is not possible to specify an image tag in the 'scheme' part of the URL, and it is always necessary to specify a tag using `TC_IMAGE_TAG`.** So that the URL becomes: `r2dbc:tc:mysql:///databasename?TC_IMAGE_TAG=8.0.36` !!! note We will use `///` (host-less URIs) from now on to emphasis the unimportance of the `host:port` pair. From Testcontainers' perspective, `r2dbc:mysql://localhost:3306/databasename` and `r2dbc:mysql:///databasename` is the same URI. !!! warning If you're using the R2DBC URL support, there is no need to instantiate an instance of the container - Testcontainers will do it automagically. ### R2DBC URL examples #### Using ClickHouse `r2dbc:tc:clickhouse:///databasename?TC_IMAGE_TAG=21.11.11-alpine` #### Using MySQL `r2dbc:tc:mysql:///databasename?TC_IMAGE_TAG=8.0.36` #### Using MariaDB `r2dbc:tc:mariadb:///databasename?TC_IMAGE_TAG=10.3.39` #### Using PostgreSQL `r2dbc:tc:postgresql:///databasename?TC_IMAGE_TAG=9.6.8` #### Using MSSQL: `r2dbc:tc:sqlserver:///?TC_IMAGE_TAG=2017-CU12` #### Using Oracle: `r2dbc:tc:oracle:///?TC_IMAGE_TAG=21-slim-faststart` ## Obtaining `ConnectionFactoryOptions` from database container objects If you already have an instance of the database container, you can get an instance of `ConnectionFactoryOptions` from it: [Creating `ConnectionFactoryOptions` from an instance)](../../../modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerTest.java) inside_block:get_options ================================================ FILE: docs/modules/databases/scylladb.md ================================================ # ScyllaDB Testcontainers module for [ScyllaDB](https://hub.docker.com/r/scylladb/scylla) ## ScyllaDB's usage examples You can start a ScyllaDB container instance from any Java application by using: [Create container](../../../modules/scylladb/src/test/java/org/testcontainers/scylladb/ScyllaDBContainerTest.java) inside_block:container [Custom config file](../../../modules/scylladb/src/test/java/org/testcontainers/scylladb/ScyllaDBContainerTest.java) inside_block:customConfiguration ### Building CqlSession [Using CQL port](../../../modules/scylladb/src/test/java/org/testcontainers/scylladb/ScyllaDBContainerTest.java) inside_block:session [Using SSL](../../../modules/scylladb/src/test/java/org/testcontainers/scylladb/ScyllaDBContainerTest.java) inside_block:sslContext [Using Shard Awareness port](../../../modules/scylladb/src/test/java/org/testcontainers/scylladb/ScyllaDBContainerTest.java) inside_block:shardAwarenessSession ### Alternator [Enabling Alternator](../../../modules/scylladb/src/test/java/org/testcontainers/scylladb/ScyllaDBContainerTest.java) inside_block:alternator [DynamoDbClient with Alternator](../../../modules/scylladb/src/test/java/org/testcontainers/scylladb/ScyllaDBContainerTest.java) inside_block:dynamodDbClient ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-scylladb:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-scylladb {{latest_version}} test ``` ================================================ FILE: docs/modules/databases/tidb.md ================================================ # TiDB Module Testcontainers module for [TiDB](https://hub.docker.com/r/pingcap/tidb) ## Usage example You can start a TiDB container instance from any Java application by using: [Container creation](../../../modules/tidb/src/test/java/org/testcontainers/tidb/TiDBContainerTest.java) inside_block:container See [Database containers](./index.md) for documentation and usage that is common to all relational database container types. ### Testcontainers JDBC URL `jdbc:tc:tidb:v6.1.0:///databasename` See [JDBC](./jdbc.md) for documentation. ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-tidb:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-tidb {{latest_version}} test ``` !!! hint Adding this Testcontainers library JAR will not automatically add a database driver JAR to your project. You should ensure that your project also has a suitable database driver as a dependency. ================================================ FILE: docs/modules/databases/timeplus.md ================================================ # Timeplus Module Testcontainers module for [Timeplus](https://hub.docker.com/r/timeplus/timeplusd) ## Usage example You can start a Timeplus container instance from any Java application by using: [Container creation](../../../modules/timeplus/src/test/java/org/testcontainers/timeplus/TimeplusContainerTest.java) inside_block:container ### Testcontainers JDBC URL `jdbc:tc:timeplus:2.3.21:///databasename` See [JDBC](./jdbc.md) for documentation. ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-timeplus:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-timeplus {{latest_version}} test ``` !!! hint Adding this Testcontainers library JAR will not automatically add a database driver JAR to your project. You should ensure that your project also has a suitable database driver as a dependency. ================================================ FILE: docs/modules/databases/trino.md ================================================ # Trino Module Testcontainers module for [Trino](https://hub.docker.com/r/trinodb/trino) ## Usage example You can start a Trino container instance from any Java application by using: [Container creation](../../../modules/trino/src/test/java/org/testcontainers/trino/TrinoContainerTest.java) inside_block:container See [Database containers](./index.md) for documentation and usage that is common to all database container types. ### Testcontainers JDBC URL `jdbc:tc:trino:352:///defaultname` See [JDBC](./jdbc.md) for documentation. ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-trino:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-trino {{latest_version}} test ``` !!! hint Adding this Testcontainers library JAR will not automatically add the Trino JDBC driver JAR to your project. You should ensure that your project has the Trino JDBC driver as a dependency, if you plan on using it. Refer to [Trino project download page](https://trino.io/download.html) for instructions. ================================================ FILE: docs/modules/databases/yugabytedb.md ================================================ # YugabyteDB Module Testcontainers module for [YugabyteDB](https://hub.docker.com/r/yugabytedb/yugabyte) See [Database containers](./index.md) for documentation and usage that is common to all database container types. YugabyteDB supports two APIs. - Yugabyte Structured Query Language [YSQL](https://docs.yugabyte.com/latest/api/ysql/) is a fully-relational API that is built by the PostgreSQL code - Yugabyte Cloud Query Language [YCQL](https://docs.yugabyte.com/latest/api/ycql/) is a semi-relational SQL API that has its roots in the Cassandra Query Language ## Usage example ### YSQL API [Creating a YSQL container](../../../modules/yugabytedb/src/test/java/org/testcontainers/junit/yugabytedb/YugabyteDBYSQLTest.java) inside_block:creatingYSQLContainer ### Testcontainers JDBC URL `jdbc:tc:yugabyte:2.14.4.0-b26:///databasename` See [JDBC](./jdbc.md) for documentation. ### YCQL API [Creating a YCQL container](../../../modules/yugabytedb/src/test/java/org/testcontainers/junit/yugabytedb/YugabyteDBYCQLTest.java) inside_block:creatingYCQLContainer ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-yugabytedb:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-yugabytedb {{latest_version}} test ``` !!! hint Adding this Testcontainers library JAR will not automatically add the Yugabytedb driver JAR to your project. You should ensure that your project has the Yugabytedb driver as a dependency, if you plan on using it. Refer to the driver page [YSQL](https://docs.yugabyte.com/latest/integrations/jdbc-driver/) and [YCQL](https://docs.yugabyte.com/latest/reference/drivers/ycql-client-drivers/) for instructions. ================================================ FILE: docs/modules/docker_compose.md ================================================ # Docker Compose Module ## Benefits 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 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. 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 `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 !!! note Make sure the service names use a `-` rather than `_` as separator. In this example, Docker Compose file should have content such as: ```yaml 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, 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. ## ComposeContainer vs DockerComposeContainer So far, we discussed `ComposeContainer`, which supports docker compose [version 2](https://www.docker.com/blog/announcing-compose-v2-general-availability/). 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) 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 ## 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` where we can specify a timeout strategy per container. 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. 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 Needless to say, we can define different strategies for each service in our Docker Compose setup. For example, our Redis container can wait for a successful redis-cli command, while our db service waits for a specific log message: [Wait for a custom command and a log message](../../core/src/test/java/org/testcontainers/junit/ComposeContainerWithWaitStrategies.java) inside_block:composeContainerWithCombinedWaitStrategies ## The 'Local Compose' Mode We 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. [Use ComposeContainer in 'Local Compose' mode](../../core/src/test/java/org/testcontainers/containers/ComposeProfilesOptionTest.java) inside_block:composeContainerWithLocalCompose ## Build Working Directory We can select what files should be copied only via `withCopyFilesInContainer`: [Use ComposeContainer in 'Local Compose' mode](../../core/src/test/java/org/testcontainers/junit/ComposeContainerWithCopyFilesTest.java) inside_block:composeContainerWithCopyFiles 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. We can use file and directory references. They are always resolved relative to the directory where the compose file resides. !!! note 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 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: * Use `DOCKER_CONFIG_FILE` environment variable. `export DOCKER_CONFIG_FILE=/some/location/config.json` * Use `dockerConfigFile` java property `java -DdockerConfigFile=/some/location/config.json` * Don't specify anything. In this case default location `$HOME/.docker/config.json`, if present, will be used. !!! note "Docker Compose and Credential Store / Credential Helpers" Modern Docker tends to store credentials using the credential store/helper mechanism rather than storing credentials in Docker's configuration file. So, your `config.json` may look something like: ```json { "auths" : { "https://index.docker.io/v1/" : { } }, "credsStore" : "osxkeychain" } ``` When run inside a container, Docker Compose cannot access the Keychain, thus making the configuration useless. To work around this problem, there are two options: ##### Putting auths in a config file Create a `config.json` in separate location with real authentication keys, like: ```json { "auths" : { "https://index.docker.io/v1/" : { "auth": "QWEADSZXC..." } }, "credsStore" : "osxkeychain" } ``` and specify the location to Testcontainers using any of the two first methods from above. ##### Using 'local compose' mode [Local Compose mode](#local-compose-mode), mentioned above, will allow compose to directly access the Docker auth system (to the same extent that running the `docker-compose` CLI manually works). ## Adding this module to your project dependencies *Docker Compose support is part of the core Testcontainers library.* Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers {{latest_version}} test ``` ================================================ FILE: docs/modules/docker_mcp_gateway.md ================================================ # Docker MCP Gateway Testcontainers module for [Docker MCP Gateway](https://hub.docker.com/r/docker/mcp-gateway). ## DockerMcpGatewayContainer's usage examples You can start a Docker MCP Gateway container instance from any Java application by using: [Create a DockerMcpGatewayContainer](../../core/src/test/java/org/testcontainers/containers/DockerMcpGatewayContainerTest.java) inside_block:container ## Adding this module to your project dependencies *Docker MCP Gateway support is part of the core Testcontainers library.* Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers {{latest_version}} test ``` ================================================ FILE: docs/modules/docker_model_runner.md ================================================ # Docker Model Runner This module helps connect to [Docker Model Runner](https://docs.docker.com/desktop/features/model-runner/) provided by Docker Desktop 4.40.0. ## DockerModelRunner's usage examples You can start a Docker Model Runner proxy container instance from any Java application by using: [Create a DockerModelRunnerContainer](../../core/src/test/java/org/testcontainers/containers/DockerModelRunnerContainerTest.java) inside_block:container ### Pulling the model Pulling the model is as simple as: [Pull model](../../core/src/test/java/org/testcontainers/containers/DockerModelRunnerContainerTest.java) inside_block:pullModel ## Adding this module to your project dependencies *Docker Model Runner support is part of the core Testcontainers library.* Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers {{latest_version}} test ``` ================================================ FILE: docs/modules/elasticsearch.md ================================================ # Elasticsearch container This module helps running [elasticsearch](https://www.elastic.co/products/elasticsearch) using Testcontainers. Note that it's based on the [official Docker image](https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html) provided by elastic. ## Usage example You can start an elasticsearch container instance from any Java application by using: [HttpClient](../../modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/ElasticsearchContainerTest.java) inside_block:httpClientContainer7 [HttpClient with Elasticsearch 8](../../modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/ElasticsearchContainerTest.java) inside_block:httpClientContainer8 [HttpClient with Elasticsearch 8 and SSL disabled](../../modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/ElasticsearchContainerTest.java) inside_block:httpClientContainerNoSSL8 [TransportClient](../../modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/ElasticsearchContainerTest.java) inside_block:transportClientContainer Note that if you are still using the [TransportClient](https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/transport-client.html) (not recommended as it is deprecated), the default cluster name is set to `docker-cluster` so you need to change `cluster.name` setting or set `client.transport.ignore_cluster_name` to `true`. ## Secure your Elasticsearch cluster The default distribution of Elasticsearch comes with the basic license which contains security feature. You can turn on security by providing a password: [HttpClient](../../modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/ElasticsearchContainerTest.java) inside_block:httpClientSecuredContainer ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-elasticsearch:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-elasticsearch {{latest_version}} test ``` ================================================ FILE: docs/modules/gcloud.md ================================================ # GCloud Module !!! note This module is INCUBATING. While it is ready for use and operational in the current version of Testcontainers, it is possible that it may receive breaking changes in the future. See [our contributing guidelines](/contributing/#incubating-modules) for more information on our incubating modules policy. Testcontainers module for the Google Cloud Platform's [Cloud SDK](https://cloud.google.com/sdk/). Currently, the module supports `BigQuery`, `Bigtable`, `Datastore`, `Firestore`, `Spanner`, and `Pub/Sub` emulators. In order to use it, you should use the following classes: Class | Container Image -|- BigQueryEmulatorContainer | [ghcr.io/goccy/bigquery-emulator](https://ghcr.io/goccy/bigquery-emulator) BigtableEmulatorContainer | [gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators](https://gcr.io/google.com/cloudsdktool/google-cloud-cli) DatastoreEmulatorContainer | [gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators](https://gcr.io/google.com/cloudsdktool/google-cloud-cli) FirestoreEmulatorContainer | [gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators](https://gcr.io/google.com/cloudsdktool/google-cloud-cli) SpannerEmulatorContainer | [gcr.io/cloud-spanner-emulator/emulator](https://gcr.io/cloud-spanner-emulator/emulator) PubSubEmulatorContainer | [gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators](https://gcr.io/google.com/cloudsdktool/google-cloud-cli) ## Usage example ### BigQuery Start BigQuery Emulator during a test: [Starting a BigQuery Emulator container](../../modules/gcloud/src/test/java/org/testcontainers/gcloud/BigQueryEmulatorContainerTest.java) inside_block:emulatorContainer [Creating BigQuery Client](../../modules/gcloud/src/test/java/org/testcontainers/gcloud/BigQueryEmulatorContainerTest.java) inside_block:bigQueryClient ### Bigtable Start Bigtable Emulator during a test: [Starting a Bigtable Emulator container](../../modules/gcloud/src/test/java/org/testcontainers/gcloud/BigtableEmulatorContainerTest.java) inside_block:emulatorContainer Create a test Bigtable table in the Emulator: [Create a test table](../../modules/gcloud/src/test/java/org/testcontainers/gcloud/BigtableEmulatorContainerTest.java) inside_block:createTable Test against the Emulator: [Testing with a Bigtable Emulator container](../../modules/gcloud/src/test/java/org/testcontainers/gcloud/BigtableEmulatorContainerTest.java) inside_block:testWithEmulatorContainer ### Datastore Start Datastore Emulator during a test: [Starting a Datastore Emulator container](../../modules/gcloud/src/test/java/org/testcontainers/gcloud/DatastoreEmulatorContainerTest.java) inside_block:creatingDatastoreEmulatorContainer And test against the Emulator: [Testing with a Datastore Emulator container](../../modules/gcloud/src/test/java/org/testcontainers/gcloud/DatastoreEmulatorContainerTest.java) inside_block:startingDatastoreEmulatorContainer See more examples: * [Full sample code](https://github.com/testcontainers/testcontainers-java/tree/main/modules/gcloud/src/test/java/org/testcontainers/gcloud/DatastoreEmulatorContainerTest.java) * [With Spring Boot](https://github.com/saturnism/testcontainers-gcloud-examples/tree/main/springboot/datastore-example/src/test/java/com/example/springboot/datastore) ### Firestore Start Firestore Emulator during a test: [Starting a Firestore Emulator container](../../modules/gcloud/src/test/java/org/testcontainers/gcloud/FirestoreEmulatorContainerTest.java) inside_block:emulatorContainer And test against the Emulator: [Testing with a Firestore Emulator container](../../modules/gcloud/src/test/java/org/testcontainers/gcloud/FirestoreEmulatorContainerTest.java) inside_block:testWithEmulatorContainer See more examples: * [Full sample code](https://github.com/testcontainers/testcontainers-java/tree/main/modules/gcloud/src/test/java/org/testcontainers/gcloud/FirestoreEmulatorContainerTest.java) * [With Spring Boot](https://github.com/saturnism/testcontainers-gcloud-examples/tree/main/springboot/firestore-example/src/test/java/com/example/springboot/firestore/FirestoreIntegrationTests.java) ### Spanner Start Spanner Emulator during a test: [Starting a Spanner Emulator container](../../modules/gcloud/src/test/java/org/testcontainers/gcloud/SpannerEmulatorContainerTest.java) inside_block:emulatorContainer Create a test Spanner Instance in the Emulator: [Create a test Spanner instance](../../modules/gcloud/src/test/java/org/testcontainers/gcloud/SpannerEmulatorContainerTest.java) inside_block:createInstance Create a test Database in the Emulator: [Creating a test Spanner database](../../modules/gcloud/src/test/java/org/testcontainers/gcloud/SpannerEmulatorContainerTest.java) inside_block:createDatabase And test against the Emulator: [Testing with a Spanner Emulator container](../../modules/gcloud/src/test/java/org/testcontainers/gcloud/SpannerEmulatorContainerTest.java) inside_block:testWithEmulatorContainer See more examples: * [Full sample code](https://github.com/testcontainers/testcontainers-java/tree/main/modules/gcloud/src/test/java/org/testcontainers/gcloud/SpannerEmulatorContainerTest.java) * [With Spring Boot](https://github.com/saturnism/testcontainers-gcloud-examples/tree/main/springboot/spanner-example/src/test/java/com/example/springboot/spanner/SpannerIntegrationTests.java) ### Pub/Sub Start Pub/Sub Emulator during a test: [Starting a Pub/Sub Emulator container](../../modules/gcloud/src/test/java/org/testcontainers/gcloud/PubSubEmulatorContainerTest.java) inside_block:emulatorContainer Create a test Pub/Sub topic in the Emulator: [Create a test topic](../../modules/gcloud/src/test/java/org/testcontainers/gcloud/PubSubEmulatorContainerTest.java) inside_block:createTopic Create a test Pub/Sub subscription in the Emulator: [Create a test subscription](../../modules/gcloud/src/test/java/org/testcontainers/gcloud/PubSubEmulatorContainerTest.java) inside_block:createSubscription And test against the Emulator: [Testing with a Pub/Sub Emulator container](../../modules/gcloud/src/test/java/org/testcontainers/gcloud/PubSubEmulatorContainerTest.java) inside_block:testWithEmulatorContainer See more examples: * [Full sample code](https://github.com/testcontainers/testcontainers-java/tree/main/modules/gcloud/src/test/java/org/testcontainers/gcloud/PubSubEmulatorContainerTest.java) * [With Spring Boot](https://github.com/saturnism/testcontainers-gcloud-examples/tree/main/springboot/pubsub-example/src/test/java/com/example/springboot/pubsub/PubSubIntegrationTests.java) ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-gcloud:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-gcloud {{latest_version}} test ``` ================================================ FILE: docs/modules/grafana.md ================================================ # Grafana Testcontainers module for [Grafana OTel LGTM](https://hub.docker.com/r/grafana/otel-lgtm). ## LGTM's usage examples You can start a Grafana OTel LGTM container instance from any Java application by using: [Grafana Otel LGTM container](../../modules/grafana/src/test/java/org/testcontainers/grafana/LgtmStackContainerTest.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:testcontainers-grafana:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-grafana {{latest_version}} test ``` ================================================ FILE: docs/modules/hivemq.md ================================================ # HiveMQ Module ![hivemq logo](../modules_logos/hivemq-module.png) Automatic starting HiveMQ docker containers for JUnit4 and JUnit5 tests. This enables testing MQTT client applications and integration testing of custom HiveMQ extensions. - Community forum: https://community.hivemq.com/ - HiveMQ website: https://www.hivemq.com/ - MQTT resources: - [MQTT Essentials](https://www.hivemq.com/mqtt-essentials/) - [MQTT 5 Essentials](https://www.hivemq.com/mqtt-5/) Please make sure to check out the hivemq-docs for the [Community Edition](https://github.com/hivemq/hivemq-community-edition/wiki/) and the [Enterprise Edition](https://www.hivemq.com/docs/hivemq/4.7/user-guide/). ## Using HiveMQ CE/EE HiveMQ provides different editions of on [Docker Hub](https://hub.docker.com/u/hivemq): - the open source [Community Edition](https://github.com/hivemq/hivemq-community-edition) which is published as *hivemq/hivemq-ce*. - the [Enterprise Edition](https://www.hivemq.com/docs/hivemq/4.7/user-guide/) which is published as *hivemq/hivemq4*. Both editions can be used directly: Using the Community Edition: [Community Edition HiveMQ image](../../modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoHiveMQContainerIT.java) inside_block:ceVersion Using the Enterprise Edition: [Enterprise Edition HiveMQ image](../../modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoHiveMQContainerIT.java) inside_block:hiveEEVersion Using a specific version is possible by using the tag: [Specific HiveMQ Version](../../modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoHiveMQContainerIT.java) inside_block:specificVersion ## Test your MQTT 3 and MQTT 5 client application Using an Mqtt-client (e.g. the [HiveMQ-Mqtt-Client](https://github.com/hivemq/hivemq-mqtt-client)) you can start testing directly. [MQTT5 Client](../../modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoHiveMQContainerIT.java) inside_block:mqtt5client ## Settings There are several things that can be adjusted before container setup. The following example shows how to enable the Control Center (this is an enterprise feature), set the log level to DEBUG and load a HiveMQ-config-file from the classpath. [Config Examples](../../modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoHiveMQContainerIT.java) inside_block:eeVersionWithControlCenter --- **Note:** The Control Center of HiveMQ can be accessed via the URL presented in the output of the starting container: ``` 2021-09-10 10:35:53,511 INFO - The HiveMQ Control Center is reachable under: http://localhost:55032 ``` Please be aware that the Control Center is a feature of the enterprise edition of HiveMQ and thus only available with the enterprise image. --- ## Testing HiveMQ extensions Using the [Extension SDK](https://github.com/hivemq/hivemq-extension-sdk) the functionality of all editions of HiveMQ can be extended. The HiveMQ module also supports testing your own custom extensions. ### Wait Strategy The raw HiveMQ module is built to wait for certain startup log messages to signal readiness. Since extensions are loaded dynamically they can be available a short while after the main container has started. We therefore provide custom wait conditions for HiveMQ Extensions: The following will specify an extension to be loaded from **src/test/resources/modifier-extension** into the container and wait for an extension named **'My Extension Name'** to be started: [Custom Wait Strategy](../../modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoExtensionTestsIT.java) inside_block:waitStrategy Next up we have an example for using an extension directly from the classpath and waiting directly on the extension: [Extension from Classpath](../../modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoExtensionTestsIT.java) inside_block:extensionClasspath --- **Note** Debugging extensions Both examples contain ```.withDebugging()``` which enables remote debugging on the container. With debugging enabled you can start putting breakpoints right into your extensions. --- ### Testing extensions using Gradle In a Gradle based HiveMQ Extension project, testing is supported using the dedicated [HiveMQ Extension Gradle Plugin](https://github.com/hivemq/hivemq-extension-gradle-plugin/README.md). The plugin adds an `integrationTest` task which executes tests from the `integrationTest` source set. - Integration test source files are defined in `src/integrationTest`. - Integration test dependencies are defined via the `integrationTestImplementation`, `integrationTestRuntimeOnly`, etc. configurations. The `integrationTest` task builds the extension and unzips it to the `build/hivemq-extension-test` directory. The tests can then load the built extension into the HiveMQ Testcontainer. [Extension from filesystem](../../modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoDisableExtensionsIT.java) inside_block:startFromFilesystem ### Enable/Disable an extension It is possible to enable and disable HiveMQ extensions during runtime. Extensions can also be disabled on startup. --- **Note**: that disabling or enabling of extension during runtime is only supported in HiveMQ 4 Enterprise Edition Containers. --- The following example shows how to start a HiveMQ container with the extension called **my-extension** being disabled. [Disable Extension at startup](../../modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoDisableExtensionsIT.java) inside_block:startDisabled The following test then proceeds to enable and then disable the extension: [Enable/Disable extension at runtime](../../modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoDisableExtensionsIT.java) inside_block:hiveRuntimeEnable ## Enable/Disable an extension loaded from a folder Extensions loaded from an extension folder during runtime can also be enabled/disabled on the fly. If the extension folder contains a DISABLED file, the extension will be disabled during startup. --- **Note**: that disabling or enabling of extension during runtime is only supported in HiveMQ 4 Enterprise Edition Containers. --- We first load the extension from the filesystem: [Extension from filesystem](../../modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoDisableExtensionsIT.java) inside_block:startFromFilesystem Now we can enable/disable the extension using its name: [Enable/Disable extension at runtime](../../modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoDisableExtensionsIT.java) inside_block:runtimeEnableFilesystem ### Remove prepackaged HiveMQ Extensions Since HiveMQ's 4.4 release, HiveMQ Docker images come with the HiveMQ Extension for Kafka, the HiveMQ Enterprise Bridge Extension and the HiveMQ Enterprise Security Extension. These Extensions are disabled by default, but sometimes you my need to remove them before the container starts. Removing all extension is as simple as: [Remove all extensions](../../modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoDisableExtensionsIT.java) inside_block:noExtensions A single extension (e.g. Kafka) can be removed as easily: [Remove a specific extension](../../modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoDisableExtensionsIT.java) inside_block:noKafkaExtension ## Put files into the container ### Put a file into HiveMQ home [Put file into HiveMQ home](../../modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoFilesIT.java) inside_block:hivemqHome ### Put files into extension home [Put file into HiveMQ-Extension home](../../modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoFilesIT.java) inside_block:extensionHome ### Put license files into the container [Put license file into the HiveMQ container](../../modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoFilesIT.java) inside_block:withLicenses ### Customize the Container further Since the `HiveMQContainer` extends from [Testcontainer's](https://github.com/testcontainers) `GenericContainer` the container can be customized as desired. ## Add to your project ### Gradle Add to `build.gradle`: ````groovy testImplementation 'org.testcontainers:testcontainers-hivemq:{{latest_version}}' ```` Add to `build.gradle.kts`: ````kotlin testImplementation("org.testcontainers:testcontainers-hivemq:{{latest_version}}") ```` ### Maven Add to `pom.xml`: ```xml org.testcontainers testcontainers-hivemq {{latest_version}} test ``` ================================================ FILE: docs/modules/k3s.md ================================================ # K3s Module !!! note This module is INCUBATING. While it is ready for use and operational in the current version of Testcontainers, it is possible that it may receive breaking changes in the future. See [our contributing guidelines](/contributing/#incubating-modules) for more information on our incubating modules policy. Testcontainers module for Rancher's [K3s](https://rancher.com/products/k3s/) lightweight Kubernetes. This module is intended to be used for testing components that interact with Kubernetes APIs - for example, operators. ## Usage example Start a K3s server as follows: [Starting a K3S server](../../modules/k3s/src/test/java/org/testcontainers/k3s/Fabric8K3sContainerTest.java) inside_block:starting_k3s ### Connecting to the server `K3sContainer` exposes a working Kubernetes client configuration, as a YAML String, via the `getKubeConfigYaml()` method. This may be used with Kubernetes clients - e.g. for the [official Java client](connecting_with_k8sio) and [the Fabric8 Kubernetes client](https://github.com/fabric8io/kubernetes-client): [Official Java client](../../modules/k3s/src/test/java/org/testcontainers/k3s/OfficialClientK3sContainerTest.java) inside_block:connecting_with_k8sio [Fabric8 Kubernetes client](../../modules/k3s/src/test/java/org/testcontainers/k3s/Fabric8K3sContainerTest.java) inside_block:connecting_with_fabric8 ## Known limitations !!! warning * K3sContainer runs as a privileged container and needs to be able to spawn its own containers. For these reasons, K3sContainer will not work in certain rootless Docker, Docker-in-Docker, or other environments where privileged containers are disallowed. * k3s containers may be unable to run on host machines where `/var/lib/docker` is on a BTRFS filesystem. See [k3s-io/k3s#4863](https://github.com/k3s-io/k3s/issues/4863) for an example. * You may experience PKIX exceptions when trying to use a configured Fabric8 client. This is down to newer distributions of k3s issuing elliptic curve keys. This can be fixed by adding [BouncyCastle PKI library](https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk15on) to your classpath. ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-k3s:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-k3s {{latest_version}} test ``` ================================================ FILE: docs/modules/k6.md ================================================ # k6 Module !!! note This module is INCUBATING. While it is ready for use and operational in the current version of Testcontainers, it is possible that it may receive breaking changes in the future. See [our contributing guidelines](/contributing/#incubating-modules) for more information on our incubating modules policy. Testcontainers module for [k6](https://registry.hub.docker.com/r/grafana/k6). [k6](https://k6.io/) is an extensible reliability testing tool built for developer happiness. ## Basic script execution You can start a K6 container instance from any Java application by using: [Setup the container](../../modules/k6/src/test/java/org/testcontainers/k6/K6ContainerTests.java) inside_block:standard_k6 The test above uses a simple k6 script, `test.js`, with command line options and an injected script variable. Once the container is started, you can wait for the test results to be collected: [Wait for test results](../../modules/k6/src/test/java/org/testcontainers/k6/K6ContainerTests.java) inside_block:wait Create a simple k6 test script to be executed as part of your tests: [Content of `scripts/test.js`](../../modules/k6/src/test/resources/scripts/test.js) inside_block:access_script_vars ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-k6:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-k6 {{latest_version}} test ``` ================================================ FILE: docs/modules/kafka.md ================================================ # Kafka Module Testcontainers can be used to automatically instantiate and manage [Apache Kafka](https://kafka.apache.org) containers. Currently, two different Kafka images are supported: * `org.testcontainers.kafka.ConfluentKafkaContainer` supports [confluentinc/cp-kafka](https://hub.docker.com/r/confluentinc/cp-kafka/) * `org.testcontainers.kafka.KafkaContainer` supports [apache/kafka](https://hub.docker.com/r/apache/kafka/) and [apache/kafka-native](https://hub.docker.com/r/apache/kafka-native/) !!! note `org.testcontainers.containers.KafkaContainer` is deprecated. Please use `org.testcontainers.kafka.ConfluentKafkaContainer` or `org.testcontainers.kafka.KafkaContainer` instead, depending on the used image. ## Benefits * Running a single node Kafka installation with just one line of code * No need to manage external Zookeeper installation, required by Kafka. ## Example ### Using org.testcontainers.kafka.KafkaContainer Create a `KafkaContainer` to use it in your tests: [Creating a KafkaContainer](../../modules/kafka/src/test/java/org/testcontainers/kafka/KafkaContainerTest.java) inside_block:constructorWithVersion Now your tests or any other process running on your machine can get access to running Kafka broker by using the following bootstrap server location: [Bootstrap Servers](../../modules/kafka/src/test/java/org/testcontainers/containers/KafkaContainerTest.java) inside_block:getBootstrapServers ### Using org.testcontainers.kafka.ConfluentKafkaContainer !!! note Compatible with `confluentinc/cp-kafka` images version `7.4.0` and later. Create a `ConfluentKafkaContainer` to use it in your tests: [Creating a ConfluentKafkaContainer](../../modules/kafka/src/test/java/org/testcontainers/kafka/ConfluentKafkaContainerTest.java) inside_block:constructorWithVersion ## Options ### Using Kraft mode !!! note Only available for `org.testcontainers.containers.KafkaContainer` KRaft mode was declared production ready in 3.3.1 (confluentinc/cp-kafka:7.3.x) [Kraft mode](../../modules/kafka/src/test/java/org/testcontainers/containers/KafkaContainerTest.java) inside_block:withKraftMode See the [versions interoperability matrix](https://docs.confluent.io/platform/current/installation/versions-interoperability.html) for more details. ### Register listeners There are scenarios where additional listeners are needed because the consumer/producer can be in another container in the same network or a different process where the port to connect differs from the default exposed port. E.g [Toxiproxy](../../modules/toxiproxy/). [Register additional listener](../../modules/kafka/src/test/java/org/testcontainers/kafka/KafkaContainerTest.java) inside_block:registerListener Container defined in the same network: [Create kcat container](../../modules/kafka/src/test/java/org/testcontainers/containers/KafkaContainerTest.java) inside_block:createKCatContainer Client using the new registered listener: [Produce/Consume via new listener](../../modules/kafka/src/test/java/org/testcontainers/containers/KafkaContainerTest.java) inside_block:produceConsumeMessage ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-kafka:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-kafka {{latest_version}} test ``` ================================================ FILE: docs/modules/ldap.md ================================================ # LDAP Testcontainers module for [LLDAP](https://hub.docker.com/r/lldap/lldap). ## LLdapContainer's usage examples You can start a LLDAP container instance from any Java application by using: [LLDAP container](../../modules/ldap/src/test/java/org/testcontainers/ldap/LLdapContainerTest.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:testcontainers-ldap:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-ldap {{latest_version}} test ``` ================================================ FILE: docs/modules/localstack.md ================================================ # LocalStack Module Testcontainers module for [LocalStack](http://localstack.cloud/), 'a fully functional local AWS cloud stack', to develop and test your cloud and serverless apps without actually using the cloud. ## Usage example You can start a LocalStack container instance from any Java application by using: [Container creation](../../modules/localstack/src/test/java/org/testcontainers/localstack/LocalStackContainerTest.java) inside_block:container ## Creating a client using AWS SDK [AWS SDK V2](../../modules/localstack/src/test/java/org/testcontainers/localstack/LocalStackContainerTest.java) inside_block:with_aws_sdk_v2 Environment variables listed in [Localstack's README](https://github.com/localstack/localstack#configurations) may be used to customize Localstack's configuration. Use the `.withEnv(key, value)` method on `LocalStackContainer` to apply configuration settings. ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-localstack:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-localstack {{latest_version}} test ``` ================================================ FILE: docs/modules/milvus.md ================================================ # Milvus Testcontainers module for [Milvus](https://hub.docker.com/r/milvusdb/milvus). ## Milvus's usage examples You can start a Milvus container instance from any Java application by using: [Default config](../../modules/milvus/src/test/java/org/testcontainers/milvus/MilvusContainerTest.java) inside_block:milvusContainer With external Etcd: [External Etcd](../../modules/milvus/src/test/java/org/testcontainers/milvus/MilvusContainerTest.java) inside_block:externalEtcd ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-milvus:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-milvus {{latest_version}} test ``` ================================================ FILE: docs/modules/minio.md ================================================ # MinIO Containers Testcontainers can be used to automatically instantiate and manage [MinIO](https://min.io) containers. ## Usage example Create a `MinIOContainer` to use it in your tests: [Starting a MinIO container](../../modules/minio/src/test/java/org/testcontainers/containers/MinIOContainerTest.java) inside_block:minioContainer The [MinIO Java client](https://min.io/docs/minio/linux/developers/java/API.html) can be configured with the container as such: [Configuring a MinIO client](../../modules/minio/src/test/java/org/testcontainers/containers/MinIOContainerTest.java) inside_block:configuringClient If needed the username and password can be overridden as such: [Overriding a MinIO container](../../modules/minio/src/test/java/org/testcontainers/containers/MinIOContainerTest.java) inside_block:minioOverrides ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-minio:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-minio {{latest_version}} test ``` ================================================ FILE: docs/modules/mockserver.md ================================================ # Mockserver Module Mock Server can be used to mock HTTP services by matching requests against user-defined expectations. ## Usage example The following example shows how to start Mockserver. [Creating a MockServer container](../../modules/mockserver/src/test/java/org/testcontainers/mockserver/MockServerContainerTest.java) inside_block:creatingProxy And how to set a simple expectation using the Java MockServerClient. [Setting a simple expectation](../../modules/mockserver/src/test/java/org/testcontainers/mockserver/MockServerContainerTest.java) inside_block:testSimpleExpectation ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-mockserver:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-mockserver {{latest_version}} test ``` Additionally, don't forget to add a [client dependency `org.mock-server:mockserver-client-java`](https://search.maven.org/search?q=mockserver-client-java) to be able to set expectations, it's not provided by the testcontainers module. Client version should match to the version in a container tag. ================================================ FILE: docs/modules/nginx.md ================================================ # Nginx Module Nginx is a web server, reverse proxy and mail proxy and http cache. ## Usage example The following example shows how to start Nginx. [Creating a Nginx container](../../modules/nginx/src/test/java/org/testcontainers/nginx/NginxContainerTest.java) inside_block:creatingContainer How to add custom content to the Nginx server. [Creating the static content to serve](../../modules/nginx/src/test/java/org/testcontainers/nginx/NginxContainerTest.java) inside_block:addCustomContent And how to query the Nginx server for the custom content added. [Creating the static content to serve](../../modules/nginx/src/test/java/org/testcontainers/nginx/NginxContainerTest.java) inside_block:getFromNginxServer ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-nginx:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-nginx {{latest_version}} test ``` ================================================ FILE: docs/modules/ollama.md ================================================ # Ollama Testcontainers module for [Ollama](https://hub.docker.com/r/ollama/ollama) . ## Ollama's usage examples You can start an Ollama container instance from any Java application by using: [Ollama container](../../modules/ollama/src/test/java/org/testcontainers/ollama/OllamaContainerTest.java) inside_block:container ### Pulling the model Testcontainers allows [executing commands in the container](../features/commands.md). So, pulling the model is as simple as: [Pull model](../../modules/ollama/src/test/java/org/testcontainers/ollama/OllamaContainerTest.java) inside_block:pullModel ### Create a new Image In order to create a new image that contains the model, you can use the following code: [Commit Image](../../modules/ollama/src/test/java/org/testcontainers/ollama/OllamaContainerTest.java) inside_block:commitToImage And use the new image along with [Image name Substitution](../features/image_name_substitution.md#manual-substitution) [Use new Image](../../modules/ollama/src/test/java/org/testcontainers/ollama/OllamaContainerTest.java) inside_block:substitute ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-ollama:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-ollama {{latest_version}} test ``` ================================================ FILE: docs/modules/openfga.md ================================================ # OpenFGA Testcontainers module for [OpenFGA](https://hub.docker.com/r/openfga/openfga). ## OpenFGAContainer's usage examples You can start an OpenFGA container instance from any Java application by using: [OpenFGA container](../../modules/openfga/src/test/java/org/testcontainers/openfga/OpenFGAContainerTest.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:testcontainers-openfga:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-openfga {{latest_version}} test ``` ================================================ FILE: docs/modules/pinecone.md ================================================ # Pinecone Testcontainers module for [Pinecone Local](https://github.com/orgs/pinecone-io/packages/container/package/pinecone-local). ## PineconeLocalContainer's usage examples You can start a Pinecone container instance from any Java application by using: [Pinecone container](../../modules/pinecone/src/test/java/org/testcontainers/pinecone/PineconeLocalContainerTest.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:testcontainers-pinecone:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-pinecone {{latest_version}} test ``` ================================================ FILE: docs/modules/pulsar.md ================================================ # Apache Pulsar Module Testcontainers can be used to automatically create [Apache Pulsar](https://pulsar.apache.org) containers without external services. It's based on the official Apache Pulsar docker image, it is recommended to read the [official guide](https://pulsar.apache.org/docs/next/getting-started-docker/). ## Example Create a `PulsarContainer` to use it in your tests: [Create a Pulsar container](../../modules/pulsar/src/test/java/org/testcontainers/pulsar/PulsarContainerTest.java) inside_block:constructorWithVersion Then you can retrieve the broker and the admin url: [Get broker and admin urls](../../modules/pulsar/src/test/java/org/testcontainers/pulsar/PulsarContainerTest.java) inside_block:coordinates ## Options ### Configuration If you need to set Pulsar configuration variables you can use the native APIs and set each variable with `PULSAR_PREFIX_` as prefix. For example, if you want to enable `brokerDeduplicationEnabled`: [Set configuration variables](../../modules/pulsar/src/test/java/org/testcontainers/pulsar/PulsarContainerTest.java) inside_block:constructorWithEnv ### Pulsar IO If you need to test Pulsar IO framework you can enable the Pulsar Functions Worker: [Create a Pulsar container with functions worker](../../modules/pulsar/src/test/java/org/testcontainers/pulsar/PulsarContainerTest.java) inside_block:constructorWithFunctionsWorker ### Pulsar Transactions If you need to test Pulsar Transactions you can enable the transactions feature: [Create a Pulsar container with transactions](../../modules/pulsar/src/test/java/org/testcontainers/pulsar/PulsarContainerTest.java) inside_block:constructorWithTransactions ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-pulsar:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-pulsar {{latest_version}} test ``` ================================================ FILE: docs/modules/qdrant.md ================================================ # Qdrant Testcontainers module for [Qdrant](https://registry.hub.docker.com/r/qdrant/qdrant) ## Qdrant's usage examples You can start a Qdrant container instance from any Java application by using: [Default QDrant container](../../modules/qdrant/src/test/java/org/testcontainers/qdrant/QdrantContainerTest.java) inside_block:qdrantContainer ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-qdrant:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-qdrant {{latest_version}} test ``` ================================================ FILE: docs/modules/rabbitmq.md ================================================ # RabbitMQ Module ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-rabbitmq:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-rabbitmq {{latest_version}} test ``` ================================================ FILE: docs/modules/redpanda.md ================================================ # Redpanda Testcontainers can be used to automatically instantiate and manage [Redpanda](https://redpanda.com/) containers. More precisely Testcontainers uses the official Docker images for [Redpanda](https://hub.docker.com/r/redpandadata/redpanda) !!! note This module uses features provided in `docker.redpanda.com/redpandadata/redpanda`. ## Example Create a `Redpanda` to use it in your tests: [Creating a Redpanda](../../modules/redpanda/src/test/java/org/testcontainers/redpanda/RedpandaContainerTest.java) inside_block:constructorWithVersion Now your tests or any other process running on your machine can get access to running Redpanda broker by using the following bootstrap server location: [Bootstrap Servers](../../modules/redpanda/src/test/java/org/testcontainers/redpanda/RedpandaContainerTest.java) inside_block:getBootstrapServers Redpanda also provides a schema registry implementation. Like the Redpanda broker, you can access by using the following schema registry location: [Schema Registry](../../modules/redpanda/src/test/java/org/testcontainers/redpanda/RedpandaContainerTest.java) inside_block:getSchemaRegistryAddress It is also possible to enable security capabilities of Redpanda by using: [Enable security](../../modules/redpanda/src/test/java/org/testcontainers/redpanda/RedpandaContainerTest.java) inside_block:security Superusers can be created by using: [Register Superuser](../../modules/redpanda/src/test/java/org/testcontainers/redpanda/RedpandaContainerTest.java) inside_block:createSuperUser Below is an example of how to create the `AdminClient`: [Create Admin Client](../../modules/redpanda/src/test/java/org/testcontainers/redpanda/RedpandaContainerTest.java) inside_block:createAdminClient There are scenarios where additional listeners are needed because the consumer/producer can be another container in the same network or a different process where the port to connect differs from the default exposed port `9092`. E.g [Toxiproxy](../modules/toxiproxy.md). [Register additional listener](../../modules/redpanda/src/test/java/org/testcontainers/redpanda/RedpandaContainerTest.java) inside_block:registerListener Container defined in the same network: [Create kcat container](../../modules/redpanda/src/test/java/org/testcontainers/redpanda/RedpandaContainerTest.java) inside_block:createKCatContainer Client using the new registered listener: [Produce/Consume via new listener](../../modules/redpanda/src/test/java/org/testcontainers/redpanda/RedpandaContainerTest.java) inside_block:produceConsumeMessage The following examples shows how to register a proxy as a new listener in `RedpandaContainer`: Use `SocatContainer` to create the proxy [Create Proxy](../../modules/redpanda/src/test/java/org/testcontainers/redpanda/RedpandaContainerTest.java) inside_block:createProxy Register the listener and advertised listener [Register Listener](../../modules/redpanda/src/test/java/org/testcontainers/redpanda/RedpandaContainerTest.java) inside_block:registerListenerAndAdvertisedListener Client using the new registered listener: [Produce/Consume via new listener](../../modules/redpanda/src/test/java/org/testcontainers/redpanda/RedpandaContainerTest.java) inside_block:produceConsumeMessageFromProxy ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-redpanda:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-redpanda {{latest_version}} test ``` ================================================ FILE: docs/modules/solace.md ================================================ # Solace Container This module helps running [Solace PubSub+](https://solace.com/products/event-broker/software/) using Testcontainers. Note that it's based on the [official Docker image](https://hub.docker.com/r/solace/solace-pubsub-standard). ## Usage example You can start a solace container instance from any Java application by using: [Solace container setup with simple authentication](../../modules/solace/src/test/java/org/testcontainers/solace/SolaceContainerSMFTest.java) inside_block:solaceContainerSetup [Solace container setup with SSL](../../modules/solace/src/test/java/org/testcontainers/solace/SolaceContainerSMFTest.java) inside_block:solaceContainerUsageSSL [Using a Solace container](../../modules/solace/src/test/java/org/testcontainers/solace/SolaceContainerAMQPTest.java) inside_block:solaceContainerUsage ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-solace:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-solace {{latest_version}} test ``` ================================================ FILE: docs/modules/solr.md ================================================ # Solr Container This module helps running [solr](https://solr.apache.org/) using Testcontainers. Note that it's based on the [official Docker image](https://hub.docker.com/_/solr/). ## Usage example You can start a solr container instance from any Java application by using: [Using a Solr container](../../modules/solr/src/test/java/org/testcontainers/solr/SolrContainerTest.java) inside_block:solrContainerUsage ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-solr:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-solr {{latest_version}} test ``` ================================================ FILE: docs/modules/toxiproxy.md ================================================ # Toxiproxy Module Testcontainers module for Shopify's [Toxiproxy](https://github.com/Shopify/toxiproxy). This TCP proxy can be used to simulate network failure conditions. You can simulate network failures: * between Java code and containers, ideal for testing resilience features of client code * between containers, for testing resilience and emergent behaviour of multi-container systems * if desired, between Java code/containers and external resources (non-Dockerized!), for scenarios where not all dependencies can be/have been dockerized Testcontainers Toxiproxy support allows resilience features to be easily verified as part of isolated dev/CI testing. This allows earlier testing of resilience features, and broader sets of failure conditions to be covered. ## Usage example A Toxiproxy container can be placed in between test code and a container, or in between containers. In either scenario, it is necessary to create a `ToxiproxyContainer` instance on the same Docker network, as follows: [Creating a Toxiproxy container](../../modules/toxiproxy/src/test/java/org/testcontainers/toxiproxy/ToxiproxyContainerTest.java) inside_block:creatingProxy Next, it is necessary to instruct Toxiproxy to start proxying connections. Each `ToxiproxyContainer` can proxy to many target containers if necessary. We do this as follows: [Starting proxying connections to a target container](../../modules/toxiproxy/src/test/java/org/testcontainers/toxiproxy/ToxiproxyContainerTest.java) inside_block:obtainProxyObject To establish a connection from the test code (on the host machine) to the target container via Toxiproxy, we obtain **Toxiproxy's** proxy host IP and port: [Obtaining proxied host and port](../../modules/toxiproxy/src/test/java/org/testcontainers/toxiproxy/ToxiproxyContainerTest.java) inside_block:obtainProxiedHostAndPortForHostMachine Code under test should connect to this proxied host IP and port. !!! note Currently, `ToxiProxyContainer` will reserve 31 ports, starting at 8666. Other containers should connect to this proxied host and port. Having done this, it is possible to trigger failure conditions ('Toxics') through the `proxy.toxics()` object: * `bandwidth` - Limit a connection to a maximum number of kilobytes per second. * `latency` - Add a delay to all data going through the proxy. The delay is equal to `latency +/- jitter`. * `slicer` - Slices TCP data up into small bits, optionally adding a delay between each sliced "packet". * `slowClose` - Delay the TCP socket from closing until `delay` milliseconds has elapsed. * `timeout` - Stops all data from getting through, and closes the connection after `timeout`. If `timeout` is `0`, the connection won't close, and data will be delayed until the toxic is removed. * `limitData` - Closes connection when transmitted data exceeded limit. Please see the [Toxiproxy documentation](https://github.com/Shopify/toxiproxy#toxics) for full details on the available Toxics. As one example, we can introduce latency and random jitter to proxied connections as follows: [Adding latency to a connection](../../modules/toxiproxy/src/test/java/org/testcontainers/toxiproxy/ToxiproxyContainerTest.java) inside_block:addingLatency Additionally we can disable the proxy to simulate a complete interruption to the network connection: [Cutting a connection](../../modules/toxiproxy/src/test/java/org/testcontainers/toxiproxy/ToxiproxyContainerTest.java) inside_block:disableProxy ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-toxiproxy:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-toxiproxy {{latest_version}} test ``` ## Acknowledgements This module was inspired by a [hotels.com blog post](https://medium.com/hotels-com-technology/i-dont-know-about-resilience-testing-and-so-can-you-b3c59d80012d). ================================================ FILE: docs/modules/typesense.md ================================================ # Typesense Testcontainers module for [Typesense](https://hub.docker.com/r/typesense/typesense). ## TypesenseContainer's usage examples You can start a 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:testcontainers-typesense:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-typesense {{latest_version}} test ``` ================================================ FILE: docs/modules/vault.md ================================================ # Hashicorp Vault Module Testcontainers module for [Vault](https://github.com/hashicorp/vault). Vault is a tool for managing secrets. More information on Vault [here](https://www.vaultproject.io/). ## Usage example Start Vault container as a `@ClassRule`: [Starting a Vault container](../../modules/vault/src/test/java/org/testcontainers/vault/VaultContainerTest.java) inside_block:vaultContainer Use CLI to read data from Vault container: [Use CLI to read data](../../modules/vault/src/test/java/org/testcontainers/vault/VaultContainerTest.java) inside_block:readFirstSecretPathWithCli Use Http API to read data from Vault container: [Use Http API to read data](../../modules/vault/src/test/java/org/testcontainers/vault/VaultContainerTest.java) inside_block:readFirstSecretPathOverHttpApi Use client library to read data from Vault container: [Use library to read data](../../modules/vault/src/test/java/org/testcontainers/vault/VaultContainerTest.java) inside_block:readWithLibrary [See full example.](https://github.com/testcontainers/testcontainers-java/blob/master/modules/vault/src/test/java/org/testcontainers/vault/VaultContainerTest.java) ## Why Vault in Junit tests? With the increasing popularity of Vault and secret management, applications are now needing to source secrets from Vault. This can prove challenging in the development phase without a running Vault instance readily on hand. This library aims to solve your apps integration testing with Vault. You can also use it to test how your application behaves with Vault by writing different test scenarios in Junit. ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-vault:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-vault {{latest_version}} test ``` ## License See [LICENSE](https://raw.githubusercontent.com/testcontainers/testcontainers-java/main/modules/vault/LICENSE). ## Copyright Copyright (c) 2017 Capital One Services, LLC and other authors. See [AUTHORS](https://raw.githubusercontent.com/testcontainers/testcontainers-java/main/modules/vault/AUTHORS) for contributors. ================================================ FILE: docs/modules/weaviate.md ================================================ # Weaviate Testcontainers module for [Weaviate](https://hub.docker.com/r/semitechnologies/weaviate) ## WeaviateContainer's usage examples You can start a Weaviate container instance from any Java application by using: [Default Weaviate container](../../modules/weaviate/src/test/java/org/testcontainers/weaviate/WeaviateContainerTest.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:testcontainers-weaviate:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-weaviate {{latest_version}} test ``` ================================================ FILE: docs/modules/webdriver_containers.md ================================================ # Webdriver Containers Testcontainers can be used to automatically instantiate and manage containers that include web browsers, such as those from SeleniumHQ's [docker-selenium](https://github.com/SeleniumHQ/docker-selenium) project. ## Benefits * Fully compatible with Selenium 3 & 4 tests for Chrome and Firefox and Selenium 4 tests for Edge, by providing a `RemoteWebDriver` instance * No need to have specific web browsers, or even a desktop environment, installed on test servers. The only dependency is a working Docker installation and your Java JUnit test suite. * Browsers are always launched from a fixed, clean image. This means no configuration drift from user changes or automatic browser upgrades. * Compatibility between browser version and the Selenium API is assured: a compatible version of the browser docker images will be automatically selected to match the version of `selenium-api-*.jar` on the classpath * Additionally the use of a clean browser prevents leakage of cookies, cached data or other state between tests. * **VNC screen recording**: Testcontainers can automatically record video of test runs (optionally capturing just failing tests) Creation of browser containers is fast, so it's actually quite feasible to have a totally fresh browser instance for every test. ## Example The following field in your JUnit UI test class will prepare a container running Chrome: [Chrome](../../modules/selenium/src/test/java/org/testcontainers/selenium/ChromeWebDriverContainerTest.java) inside_block:junitRule Now, instead of instantiating an instance of WebDriver directly, use the following to obtain an instance inside your test methods: [RemoteWebDriver](../../modules/selenium/src/test/java/org/testcontainers/selenium/LocalServerWebDriverContainerTest.java) inside_block:getWebDriver You can then use this driver instance like a regular WebDriver. Note that, if you want to test a **web application running on the host machine** (the machine the JUnit tests are running on - which is quite likely), you'll need to use [the host exposing](../features/networking.md#exposing-host-ports-to-the-container) feature of Testcontainers, e.g.: [Open Web Page](../../modules/selenium/src/test/java/org/testcontainers/selenium/LocalServerWebDriverContainerTest.java) inside_block:getPage ## Options ### Other browsers At the moment, Chrome, Firefox and Edge are supported. To switch, simply change the first parameter to the rule constructor: [Chrome](../../modules/selenium/src/test/java/org/testcontainers/selenium/ChromeWebDriverContainerTest.java) inside_block:junitRule [Firefox](../../modules/selenium/src/test/java/org/testcontainers/selenium/FirefoxWebDriverContainerTest.java) inside_block:junitRule [Edge](../../modules/selenium/src/test/java/org/testcontainers/selenium/EdgeWebDriverContainerTest.java) inside_block:junitRule ### Recording videos By default, no videos will be recorded. However, you can instruct Testcontainers to capture videos for all tests or just for failing tests. [Record all Tests](../../modules/selenium/src/test/java/org/testcontainers/selenium/ChromeRecordingWebDriverContainerTest.java) inside_block:recordAll [Record failing Tests](../../modules/selenium/src/test/java/org/testcontainers/selenium/ChromeRecordingWebDriverContainerTest.java) inside_block:recordFailing Note that the second parameter of `withRecordingMode` should be a directory where recordings can be saved. By default, the video will be recorded in [FLV](https://en.wikipedia.org/wiki/Flash_Video) format, but you can specify it explicitly or change it to [MP4](https://en.wikipedia.org/wiki/MPEG-4_Part_14) using `withRecordingMode` method with `VncRecordingFormat` option: [Video Format in MP4](../../modules/selenium/src/test/java/org/testcontainers/selenium/ChromeRecordingWebDriverContainerTest.java) inside_block:recordMp4 [Video Format in FLV](../../modules/selenium/src/test/java/org/testcontainers/selenium/ChromeRecordingWebDriverContainerTest.java) inside_block:recordFlv If you would like to customise the file name of the recording, or provide a different directory at runtime based on the description of the test and/or its success or failure, you may provide a custom recording file factory as follows: [CustomRecordingFileFactory](../../modules/selenium/src/test/java/org/testcontainers/selenium/ChromeRecordingWebDriverContainerTest.java) inside_block:withRecordingFileFactory Note the factory must implement `org.testcontainers.containers.RecordingFileFactory`. ## More examples A few different examples are shown in [ChromeWebDriverContainerTest.java](https://github.com/testcontainers/testcontainers-java/blob/main/modules/selenium/src/test/java/org/testcontainers/selenium/ChromeWebDriverContainerTest.java). ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-selenium:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-selenium {{latest_version}} test ``` !!! hint Adding this Testcontainers library JAR will not automatically add a Selenium Webdriver JAR to your project. You should ensure that your project also has suitable Selenium dependencies in place, for example: === "Gradle" ```groovy compile "org.seleniumhq.selenium:selenium-remote-driver:3.141.59" ``` === "Maven" ```xml org.seleniumhq.selenium selenium-remote-driver 3.141.59 ``` Testcontainers will try and match the version of the Dockerized browser to whichever version of Selenium is found on the classpath ================================================ FILE: docs/quickstart/junit_4_quickstart.md ================================================ # JUnit 4 Quickstart It's easy to add Testcontainers to your project - let's walk through a quick example to see how. Let's imagine we have a simple program that has a dependency on Redis, and we want to add some tests for it. In our imaginary program, there is a `RedisBackedCache` class which stores data in Redis. You can see an example test that could have been written for it (without using Testcontainers): [Pre-Testcontainers test code](../examples/junit4/redis/src/test/java/quickstart/RedisBackedCacheIntTestStep0.java) block:RedisBackedCacheIntTestStep0 Notice that the existing test has a problem - it's relying on a local installation of Redis, which is a red flag for test reliability. This may work if we were sure that every developer and CI machine had Redis installed, but would fail otherwise. We might also have problems if we attempted to run tests in parallel, such as state bleeding between tests, or port clashes. Let's start from here, and see how to improve the test with Testcontainers: ## 1. Add Testcontainers as a test-scoped dependency First, add Testcontainers as a dependency as follows: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers {{latest_version}} test ``` ## 2. Get Testcontainers to run a Redis container during our tests Simply add the following to the body of our test class: [JUnit 4 Rule](../examples/junit4/redis/src/test/java/quickstart/RedisBackedCacheIntTest.java) inside_block:rule The `@Rule` annotation tells JUnit to notify this field about various events in the test lifecycle. In this case, our rule object is a Testcontainers `GenericContainer`, configured to use a specific Redis image from Docker Hub, and configured to expose a port. If we run our test as-is, then regardless of the actual test outcome, we'll see logs showing us that Testcontainers: * was activated before our test method ran * discovered and quickly tested our local Docker setup * pulled the image if necessary * started a new container and waited for it to be ready * shut down and deleted the container after the test ## 3. Make sure our code can talk to the container Before Testcontainers, we might have hardcoded an address like `localhost:6379` into our tests. Testcontainers uses *randomized ports* for each container it starts, but makes it easy to obtain the actual port at runtime. We can do this in our test `setUp` method, to set up our component under test: [Obtaining a mapped port](../examples/junit4/redis/src/test/java/quickstart/RedisBackedCacheIntTest.java) inside_block:setUp !!! tip Notice that we also ask Testcontainers for the container's actual address with `redis.getHost();`, rather than hard-coding `localhost`. `localhost` may work in some environments but not others - for example it may not work on your current or future CI environment. As such, **avoid hard-coding** the address, and use `getHost()` instead. ## 4. Run the tests! That's it! Let's look at our complete test class to see how little we had to add to get up and running with Testcontainers: [RedisBackedCacheIntTest](../examples/junit4/redis/src/test/java/quickstart/RedisBackedCacheIntTest.java) block:RedisBackedCacheIntTest ================================================ FILE: docs/quickstart/junit_5_quickstart.md ================================================ # JUnit 5 Quickstart It's easy to add Testcontainers to your project - let's walk through a quick example to see how. Let's imagine we have a simple program that has a dependency on Redis, and we want to add some tests for it. In our imaginary program, there is a `RedisBackedCache` class which stores data in Redis. You can see an example test that could have been written for it (without using Testcontainers): [Pre-Testcontainers test code](../examples/junit5/redis/src/test/java/quickstart/RedisBackedCacheIntTestStep0.java) block:RedisBackedCacheIntTestStep0 Notice that the existing test has a problem - it's relying on a local installation of Redis, which is a red flag for test reliability. This may work if we were sure that every developer and CI machine had Redis installed, but would fail otherwise. We might also have problems if we attempted to run tests in parallel, such as state bleeding between tests, or port clashes. Let's start from here, and see how to improve the test with Testcontainers: ## 1. Add Testcontainers as a test-scoped dependency First, add Testcontainers as a dependency as follows: === "Gradle" ```groovy testImplementation "org.junit.jupiter:junit-jupiter:5.8.1" testImplementation "org.testcontainers:testcontainers:{{latest_version}}" testImplementation "org.testcontainers:testcontainers-junit-jupiter:{{latest_version}}" ``` === "Maven" ```xml org.junit.jupiter junit-jupiter 5.8.1 test org.testcontainers testcontainers {{latest_version}} test org.testcontainers testcontainers-junit-jupiter {{latest_version}} test ``` ## 2. Get Testcontainers to run a Redis container during our tests First, you'll need to annotate the test class with `@Testcontainers`. Furthermore, add the following to the body of our test class: [JUnit 5 Rule](../examples/junit5/redis/src/test/java/quickstart/RedisBackedCacheIntTest.java) inside_block:container The `@Container` annotation tells JUnit to notify this field about various events in the test lifecycle. In this case, our rule object is a Testcontainers `GenericContainer`, configured to use a specific Redis image from Docker Hub, and configured to expose a port. If we run our test as-is, then regardless of the actual test outcome, we'll see logs showing us that Testcontainers: * was activated before our test method ran * discovered and quickly tested our local Docker setup * pulled the image if necessary * started a new container and waited for it to be ready * shut down and deleted the container after the test ## 3. Make sure our code can talk to the container Before Testcontainers, we might have hardcoded an address like `localhost:6379` into our tests. Testcontainers uses *randomized ports* for each container it starts, but makes it easy to obtain the actual port at runtime. We can do this in our test `setUp` method, to set up our component under test: [Obtaining a mapped port](../examples/junit5/redis/src/test/java/quickstart/RedisBackedCacheIntTest.java) inside_block:setUp !!! tip Notice that we also ask Testcontainers for the container's actual address with `redis.getHost();`, rather than hard-coding `localhost`. `localhost` may work in some environments but not others - for example it may not work on your current or future CI environment. As such, **avoid hard-coding** the address, and use `getHost()` instead. ## 4. Additional attributes Additional attributes are available for the `@Testcontainers` annotation. Those attributes can be helpful when: * Tests should be skipped instead of failing because Docker is unavailable in the current environment. Set `disabledWithoutDocker` to `true`. * Enable parallel container initialization instead of sequential (by default). Set `parallel` to `true`. ## 5. Run the tests! That's it! Let's look at our complete test class to see how little we had to add to get up and running with Testcontainers: [RedisBackedCacheIntTest](../examples/junit5/redis/src/test/java/quickstart/RedisBackedCacheIntTest.java) inside_block:class ================================================ FILE: docs/quickstart/spock_quickstart.md ================================================ # Spock Quickstart It's easy to add Testcontainers to your project - let's walk through a quick example to see how. Let's imagine we have a simple program that has a dependency on Redis, and we want to add some tests for it. In our imaginary program, there is a `RedisBackedCache` class which stores data in Redis. You can see an example test that could have been written for it (without using Testcontainers): [Pre-Testcontainers test code](../examples/spock/redis/src/test/groovy/quickstart/RedisBackedCacheIntTestStep0.groovy) block:RedisBackedCacheIntTestStep0 Notice that the existing test has a problem - it's relying on a local installation of Redis, which is a red flag for test reliability. This may work if we were sure that every developer and CI machine had Redis installed, but would fail otherwise. We might also have problems if we attempted to run tests in parallel, such as state bleeding between tests, or port clashes. Let's start from here, and see how to improve the test with Testcontainers: ## 1. Add Testcontainers as a test-scoped dependency First, add Testcontainers as a dependency as follows: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-spock:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-spock {{latest_version}} test ``` ## 2. Get Testcontainers to run a Redis container during our tests Annotate the Spock specification class with the Testcontainers extension: === "Spock Testcontainers annotation" ```groovy @org.testcontainers.spock.Testcontainers class RedisBackedCacheIntTest extends Specification { ``` And add the following field to the body of our test class: [Spock Testcontainers init](../examples/spock/redis/src/test/groovy/quickstart/RedisBackedCacheIntTest.groovy) inside_block:init This tells Spock to start a Testcontainers `GenericContainer`, configured to use a specific Redis image from Docker Hub, and configured to expose a port. If we run our test as-is, then regardless of the actual test outcome, we'll see logs showing us that Testcontainers: * was activated before our test method ran * discovered and quickly tested our local Docker setup * pulled the image if necessary * started a new container and waited for it to be ready * shut down and deleted the container after the test ## 3. Make sure our code can talk to the container Before Testcontainers, we might have hardcoded an address like `localhost:6379` into our tests. Testcontainers uses *randomized ports* for each container it starts, but makes it easy to obtain the actual port at runtime. We can do this in our test `setup` method, to set up our component under test: [Obtaining a mapped port](../examples/spock/redis/src/test/groovy/quickstart/RedisBackedCacheIntTest.groovy) inside_block:setup !!! tip Notice that we also ask Testcontainers for the container's actual address with `redis.containerIpAddress`, rather than hard-coding `localhost`. `localhost` may work in some environments but not others - for example it may not work on your current or future CI environment. As such, **avoid hard-coding** the address, and use `containerIpAddress` instead. ## 4. Run the tests! That's it! Let's look at our complete test class to see how little we had to add to get up and running with Testcontainers: [RedisBackedCacheIntTest](../examples/spock/redis/src/test/groovy/quickstart/RedisBackedCacheIntTest.groovy) block:complete ================================================ FILE: docs/supported_docker_environment/continuous_integration/aws_codebuild.md ================================================ # AWS CodeBuild To enable access to Docker in AWS CodeBuild, go to `Privileged` section and check `Enable this flag if you want to build Docker images or want your builds to get elevated privileges`. This is a sample `buildspec.yml` config: ```yaml version: 0.2 phases: install: runtime-versions: java: corretto17 build: commands: - ./mvnw test ``` ================================================ FILE: docs/supported_docker_environment/continuous_integration/bitbucket_pipelines.md ================================================ # Bitbucket Pipelines To enable access to Docker in Bitbucket Pipelines, you need to add `docker` as a service on the step. Furthermore, Ryuk needs to be turned off since Bitbucket Pipelines does not allow starting privileged containers (see [Disabling Ryuk](../../features/configuration.md#disabling-ryuk)). This can either be done by setting a repository variable in Bitbucket's project settings or by explicitly exporting the variable on a step. In some cases the memory available to Docker needs to be increased. Here is a sample Bitbucket Pipeline configuration that does a checkout of a project and runs maven: ```yml image: maven:3.6.1 pipelines: default: - step: script: - export TESTCONTAINERS_RYUK_DISABLED=true - mvn clean install services: - docker definitions: services: docker: memory: 2048 ``` ================================================ FILE: docs/supported_docker_environment/continuous_integration/circle_ci.md ================================================ # CircleCI (Cloud, Server v2.x, and Server v3.x) Your CircleCI configuration should use a dedicated VM for Testcontainers to work. You can achieve this by specifying the executor type in your `.circleci/config.yml` to be `machine` instead of the default `docker` executor (see [Choosing an Executor Type](https://circleci.com/docs/executor-intro) for more info). Here is a sample CircleCI configuration that does a checkout of a project and runs Maven: ```yml jobs: build: # Check https://circleci.com/docs/executor-intro#linux-vm for more details machine: true steps: - checkout - run: mvn -B clean install ``` You can learn more about the best practices of using Testcontainers together with CircleCI in [this article](https://www.atomicjar.com/2022/12/testcontainers-with-circleci/). ================================================ FILE: docs/supported_docker_environment/continuous_integration/concourse_ci.md ================================================ # Concourse CI This is an example to run Testcontainers tests on [Concourse CI](https://concourse-ci.org/). A possible `pipeline.yml` config looks like this: ```yaml resources: - name: repo type: git source: uri: https://github.com/testcontainers/testcontainers-java-repro.git jobs: - name: testcontainers-job plan: # Add a get step referencing the resource - get: repo - task: testcontainers-task privileged: true config: platform: linux image_resource: type: docker-image source: repository: amidos/dcind tag: 2.1.0 inputs: - name: repo run: path: /bin/sh args: - -c - | source /docker-lib.sh start_docker cd repo docker run -it --rm -v "$PWD:$PWD" -w "$PWD" -v /var/run/docker.sock:/var/run/docker.sock eclipse-temurin:17.0.5_8-jdk-alpine ./mvnw clean package ``` ```bash fly -t tutorial set-pipeline -p testcontainers-pipeline -c pipeline.yml fly -t tutorial unpause-pipeline -p testcontainers-pipeline fly -t tutorial trigger-job --job testcontainers-pipeline/testcontainers-job --watch ``` ================================================ FILE: docs/supported_docker_environment/continuous_integration/dind_patterns.md ================================================ # Patterns for running tests inside a Docker container ## 'Docker wormhole' pattern - Sibling docker containers Testcontainers itself can be used from inside a container. This is very useful for different CI scenarios like running everything in containers on Jenkins, or Docker-based CI tools such as Drone. Testcontainers will automatically detect if it's inside a container and instead of "localhost" will use the default gateway's IP. However, additional configuration is required if you use [volume mapping](../../features/files.md). The following points need to be considered: * The docker socket must be available via a volume mount * The 'local' source code directory must be volume mounted *at the same path* inside the container that Testcontainers runs in, so that Testcontainers is able to set up the correct volume mounts for the containers it spawns. ### Docker-only example If you run the tests with just `docker run ...` then make sure you add `-v $PWD:$PWD -w $PWD -v /var/run/docker.sock:/var/run/docker.sock` to the command, so it will look like this: ```bash $ tree . . ├── pom.xml └── src └── test └── java └── MyTestWithTestcontainers.java $ docker run -it --rm -v $PWD:$PWD -w $PWD -v /var/run/docker.sock:/var/run/docker.sock maven:3 mvn test ``` Where: * `-v $PWD:$PWD` will add your current directory as a volume inside the container * `-w $PWD` will set the current directory to this volume * `-v /var/run/docker.sock:/var/run/docker.sock` will map the Docker socket !!! note If you are using Docker Desktop, you need to configure the `TESTCONTAINERS_HOST_OVERRIDE` environment variable to use the special DNS name `host.docker.internal` for accessing the host from within a container, which is provided by Docker Desktop: `-e TESTCONTAINERS_HOST_OVERRIDE=host.docker.internal` ### Docker Compose example The same can be achieved with Docker Compose: ```yaml tests: image: maven:3 stop_signal: SIGKILL stdin_open: true tty: true working_dir: $PWD volumes: - $PWD:$PWD - /var/run/docker.sock:/var/run/docker.sock # Maven cache (optional) - ~/.m2:/root/.m2 command: mvn test ``` ## Docker-in-Docker While Docker-in-Docker (DinD) is generally considered an instrument of last resort, it is necessary for some CI environments. [Drone CI](./drone.md) is one such example. Testcontainers has a Docker-in-Docker plugin (build image) for use with Drone, which could be used as inspiration for setting up other similar testing using DinD. ================================================ FILE: docs/supported_docker_environment/continuous_integration/drone.md ================================================ # Drone CI Drone CI 0.8 is supported via the use of a general purpose Docker-in-Docker plugin. Please see [testcontainers/dind-drone-plugin](https://github.com/testcontainers/dind-drone-plugin) for further details and usage instructions. ================================================ FILE: docs/supported_docker_environment/continuous_integration/gitlab_ci.md ================================================ # GitLab CI ## Example using Docker socket This applies if you have your own GitlabCI runner installed, use the Docker executor and you have `/var/run/docker.sock` mounted in the runner configuration. See below for an example runner configuration: ```toml [[runners]] name = "MACHINE_NAME" url = "https://gitlab.com/" token = "GENERATED_GITLAB_RUNNER_TOKEN" executor = "docker" [runners.docker] tls_verify = false image = "docker:latest" privileged = false disable_entrypoint_overwrite = false oom_kill_disable = false disable_cache = false volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"] shm_size = 0 ``` Please also include the following in your GitlabCI pipeline definitions (`.gitlab-ci.yml`) that use Testcontainers: ```yml variables: TESTCONTAINERS_HOST_OVERRIDE: "" ``` The environment variable `TESTCONTAINERS_HOST_OVERRIDE` needs to be configured, otherwise, a wrong IP address would be used to resolve the Docker host, which will likely lead to failing tests. For Windows and MacOS, use `host.docker.internal`. ## Example using DinD (Docker-in-Docker) In order to use Testcontainers in a Gitlab CI pipeline, you need to run the job as a Docker container (see [Patterns for running inside Docker](dind_patterns.md)). So edit your `.gitlab-ci.yml` to include the [Docker-In-Docker service](https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#use-docker-in-docker-workflow-with-docker-executor) (`docker:dind`) and set the `DOCKER_HOST` variable to `tcp://docker:2375` and `DOCKER_TLS_CERTDIR` to empty string. Caveat: Current docker releases (verified for 20.10.9) intentionally delay the startup, if the docker api is bound to a network address but not TLS protected. To avoid this delay, the docker process needs to be started with the argument `--tls=false`. Otherwise jobs which access the docker api at the very beginning might fail. Here is a sample `.gitlab-ci.yml` that executes test with gradle: ```yml # DinD service is required for Testcontainers services: - name: docker:dind # explicitly disable tls to avoid docker startup interruption command: ["--tls=false"] variables: # Instruct Testcontainers to use the daemon of DinD, use port 2375 for non-tls connections. DOCKER_HOST: "tcp://docker:2375" # Instruct Docker not to start over TLS. DOCKER_TLS_CERTDIR: "" # Improve performance with overlayfs. DOCKER_DRIVER: overlay2 test: image: gradle:5.0 stage: test script: ./gradlew test ``` ================================================ FILE: docs/supported_docker_environment/continuous_integration/tekton.md ================================================ # Tekton To enable access to Docker in Tekton, a dind sidecar needs to be added. An example of it can be found [here](https://github.com/tektoncd/pipeline/blob/main/examples/v1beta1/taskruns/dind-sidecar.yaml) This is an example ```yaml apiVersion: tekton.dev/v1beta1 kind: Task metadata: name: run-tests description: Run Tests spec: workspaces: - name: source steps: - name: read image: eclipse-temurin:17.0.3_7-jdk-alpine workingDir: $(workspaces.source.path) script: ./mvnw test volumeMounts: - mountPath: /var/run/ name: dind-socket sidecars: - image: docker:20.10-dind name: docker securityContext: privileged: true volumeMounts: - mountPath: /var/lib/docker name: dind-storage - mountPath: /var/run/ name: dind-socket volumes: - name: dind-storage emptyDir: { } - name: dind-socket emptyDir: { } --- apiVersion: tekton.dev/v1beta1 kind: Pipeline metadata: name: testcontainers-demo spec: description: | This pipeline clones a git repo, run testcontainers. params: - name: repo-url type: string description: The git repo URL to clone from. workspaces: - name: shared-data description: | This workspace contains the cloned repo files, so they can be read by the next task. tasks: - name: fetch-source taskRef: name: git-clone workspaces: - name: output workspace: shared-data params: - name: url value: $(params.repo-url) - name: run-tests runAfter: ["fetch-source"] taskRef: name: run-tests workspaces: - name: source workspace: shared-data --- apiVersion: tekton.dev/v1beta1 kind: PipelineRun metadata: name: testcontainers-demo-run spec: pipelineRef: name: testcontainers-demo workspaces: - name: shared-data volumeClaimTemplate: spec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi params: - name: repo-url value: https://github.com/testcontainers/testcontainers-java-repro.git ``` ================================================ FILE: docs/supported_docker_environment/continuous_integration/travis.md ================================================ # Travis To run Testcontainers on TravisCI, docker needs to be installed. The configuration below is the minimal required config. ```yaml language: java jdk: - openjdk8 services: - docker script: ./mvnw verify ``` ================================================ FILE: docs/supported_docker_environment/image_registry_rate_limiting.md ================================================ # Image Registry rate limiting As of November 2020 Docker Hub pulls are rate limited. As Testcontainers uses Docker Hub for standard images, some users may hit these rate limits and should mitigate accordingly. Suggested mitigations are noted in [this issue](https://github.com/testcontainers/testcontainers-java/issues/3099) at present. ## Which images are used by Testcontainers? As of the current version of Testcontainers ({{latest_version}}): * every image directly used by your tests * images pulled by Testcontainers itself to support functionality: * [`testcontainers/ryuk`](https://hub.docker.com/r/testcontainers/ryuk) - performs fail-safe cleanup of containers, and always required (unless [Ryuk is disabled](../features/configuration.md#disabling-ryuk)) * [`alpine`](https://hub.docker.com/r/_/alpine) - used to check whether images can be pulled at startup, and always required (unless [startup checks are disabled](../features/configuration.md#disabling-the-startup-checks)) * [`testcontainers/sshd`](https://hub.docker.com/r/testcontainers/sshd) - required if [exposing host ports to containers](../features/networking.md#exposing-host-ports-to-the-container) * [`testcontainers/vnc-recorder`](https://hub.docker.com/r/testcontainers/vnc-recorder) - required if using [Webdriver containers](../modules/webdriver_containers.md) and using the screen recording feature * [`docker/compose`](https://hub.docker.com/r/docker/compose) - required if using [Docker Compose](../modules/docker_compose.md) * [`alpine/socat`](https://hub.docker.com/r/alpine/socat) - required if using [Docker Compose](../modules/docker_compose.md) ================================================ FILE: docs/supported_docker_environment/index.md ================================================ # General Container runtime requirements ## Overview To run Testcontainers-based tests, you need a Docker-API compatible container runtime, such as using [Testcontainers Cloud](https://www.testcontainers.cloud/) or installing Docker locally. During development, Testcontainers is actively tested against recent versions of Docker on Linux, as well as against Docker Desktop on Mac and Windows. These Docker environments are automatically detected and used by Testcontainers without any additional configuration being necessary. It is possible to configure Testcontainers to work with alternative container runtimes. Making use of the free [Testcontainers Desktop](https://testcontainers.com/desktop/) app will take care of most of the manual configuration. When using those alternatives without Testcontainers Desktop, sometimes some manual configuration might be necessary (see further down for specific runtimes, or [Customizing Docker host detection](/features/configuration/#customizing-docker-host-detection) for general configuration mechanisms). Alternative container runtimes are not actively tested in the main development workflow, so not all Testcontainers features might be available. If you have further questions about configuration details for your setup or whether it supports running Testcontainers-based tests, please contact the Testcontainers team and other users from the Testcontainers community on [Slack](https://slack.testcontainers.org/). ## Colima In order to run testcontainers against [colima](https://github.com/abiosoft/colima) the env vars below should be set ```bash colima start --network-address export TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock export TESTCONTAINERS_HOST_OVERRIDE=$(colima ls -j | jq -r '.address') export DOCKER_HOST="unix://${HOME}/.colima/default/docker.sock" ``` ## Podman In order to run testcontainers against [podman](https://podman.io/) the env vars bellow should be set MacOS: ```bash {% raw %} export DOCKER_HOST=unix://$(podman machine inspect --format '{{.ConnectionInfo.PodmanSocket.Path}}') export TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock {% endraw %} ``` Linux: ```bash export DOCKER_HOST=unix://${XDG_RUNTIME_DIR}/podman/podman.sock ``` If you're running Podman in rootless mode, ensure to include the following line to disable Ryuk: ```bash export TESTCONTAINERS_RYUK_DISABLED=true ``` !!! note Previous to version 1.19.0, `export TESTCONTAINERS_RYUK_PRIVILEGED=true` was required for rootful mode. Starting with 1.19.0, this is no longer required. ## Rancher Desktop In order to run testcontainers against [Rancher Desktop](https://rancherdesktop.io/) the env vars below should be set. If you're running Rancher Desktop as an administrator in a MacOS (M1) machine: Using QEMU emulation ```bash export TESTCONTAINERS_HOST_OVERRIDE=$(rdctl shell ip a show rd0 | awk '/inet / {sub("/.*",""); print $2}') ``` Using VZ emulation ```bash export TESTCONTAINERS_HOST_OVERRIDE=$(rdctl shell ip a show vznat | awk '/inet / {sub("/.*",""); print $2}') ``` If you're not running Rancher Desktop as an administrator in a MacOS (M1) machine: Using VZ emulation ```bash export DOCKER_HOST=unix://$HOME/.rd/docker.sock export TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock export TESTCONTAINERS_HOST_OVERRIDE=$(rdctl shell ip a show vznat | awk '/inet / {sub("/.*",""); print $2}') ``` ## Docker environment discovery Testcontainers will try to connect to a Docker daemon using the following strategies in order: * Environment variables: * `DOCKER_HOST` * `DOCKER_TLS_VERIFY` * `DOCKER_CERT_PATH` * Defaults: * `DOCKER_HOST=https://localhost:2376` * `DOCKER_TLS_VERIFY=1` * `DOCKER_CERT_PATH=~/.docker` * If Docker Machine is installed, the docker machine environment for the *first* machine found. Docker Machine needs to be on the PATH for this to succeed. * If you're going to run your tests inside a container, please read [Patterns for running tests inside a docker container](continuous_integration/dind_patterns.md) first. ## Docker registry authentication Testcontainers will try to authenticate to registries with supplied config using the following strategies in order: * Environment variables: * `DOCKER_AUTH_CONFIG` * Docker config * At location specified in `DOCKER_CONFIG` or at `{HOME}/.docker/config.json` ================================================ FILE: docs/supported_docker_environment/logging_config.md ================================================ # Recommended logback configuration Testcontainers, and many of the libraries it uses, utilize SLF4J for logging. In order to see logs from Testcontainers, your project should include an SLF4J implementation (Logback is recommended). The following example `logback-test.xml` should be included in your classpath to show a reasonable level of log output: ```xml %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n ``` In order to troubleshoot issues with Testcontainers, increase the logging level of `org.testcontainers` to `DEBUG`: ```xml ``` Avoid changing the root logger's level to `DEBUG`, because this turns on debug logging for every package whose level isn't explicitly configured here, resulting in a large amount of log data. ================================================ FILE: docs/supported_docker_environment/windows.md ================================================ # Windows Support ## Prerequisites * [Docker for Windows](https://docs.docker.com/docker-for-windows/) needs to be installed * Docker version 17.06 is confirmed to work on Windows 10 with Hyper-V. * Testcontainers supports communication with Docker on Docker for Windows using named pipes. * WSL2 backend is supported starting with Windows 10 2004. (**Beta**) * Docker on Windows Server 2019 is currently **not supported** (also note [this issue](https://github.com/testcontainers/testcontainers-java/issues/2960)). ## Limitations The following features are not available or do not work correctly so make sure you do not use them or use them with caution. The list may not be complete. ### MySQL containers * MySQL server prevents custom configuration file (ini-script) from being loaded due to security measures ([link to feature description](../modules/databases/index.md#using-an-init-script-from-a-file)) ### Windows Container on Windows (WCOW) * WCOW is currently not supported, since Testcontainers uses auxiliary Linux containers for certain tasks and Docker for Windows does not support hybrid engine mode at the time of writing. ## WSL2 backend Using Docker for Windows with WSL2 backend should work out of the box. However, there is an [existing issue](https://github.com/microsoft/WSL/issues/4694) in WSL/WSL2 that effects certain older Docker images. The currently proposed workaround is to enable vsyscall emulation in the WSL2 kernel: ``` [wsl2] kernelCommandLine = vsyscall=emulate ``` ## Windows Subsystem for Linux (WSL) Testcontainers supports communicating with Docker for Windows within the Windows Subsystem for Linux *([**WSL**](https://docs.microsoft.com/en-us/windows/wsl/about))*. The following additional configurations steps are required: + Expose the Docker for Windows daemon on tcp port `2375` without **TLS**. *(Right-click the Docker for Windows icon on the task bar, click setting and go to `General`)*. + Set the `DOCKER_HOST` environment variable inside the **WSL** shell to `tcp://localhost:2375`. It is recommended to add this to your `~/.bashrc` file, so it’s available every time you open your terminal. + **Optional** - Only if volumes are required: Inside the **WSL** shell, modify the `/ect/wsl.conf` file to mount the Windows drives on `/` instead of on `/mnt/`. *(Reboot required after this step)*. Remember to share the drives, on which you will store your volumes, with Docker for Windows. *(Right-click the Docker for Windows icon on the task bar, click setting and go to `Shared Drives`)*. More information about running Docker within the **WSL** can be found [here](https://nickjanetakis.com/blog/setting-up-docker-for-windows-and-wsl-to-work-flawlessly). ## Reporting issues Please report any issues with the Windows build of Testcontainers [here](https://github.com/testcontainers/testcontainers-java/issues) and be sure to note that you are using this on Windows. ================================================ FILE: docs/test_framework_integration/external.md ================================================ # External Integrations The following Open Source frameworks add direct integration to Testcontainers | Framework | Source Code | Documentation | | --- | --- | --- | | jqwik | [jqwik-testcontainers](https://github.com/jqwik-team/jqwik-testcontainers) | [README](https://github.com/jqwik-team/jqwik-testcontainers) | | Kotest | [Kotest Extensions Testcontainers](https://github.com/kotest/kotest/tree/master/kotest-extensions/kotest-extensions-testcontainers) | [kotest.io](https://kotest.io/docs/extensions/test_containers.html) | | Synthesized | [Synthesized TDK-Testcontainers integration](https://github.com/synthesized-io/tdk-tc) | [synthesized.io](https://docs.synthesized.io/tdk/latest/user_guide/integrations/testcontainers) | | TCI | [Testcontainers Infrastructure (TCI) Framework](https://github.com/xdev-software/tci-base) | [README](https://github.com/xdev-software/tci-base) | ================================================ FILE: docs/test_framework_integration/junit_4.md ================================================ # JUnit 4 ## `@Rule`/`@ClassRule` integration **JUnit4 `@Rule`/`@ClassRule`**: This mode starts the container before your tests and tears it down afterwards. Add a `@Rule` or `@ClassRule` annotated field to your test class, e.g.: ```java public class SimpleMySQLTest { @Rule public MySQLContainer mysql = new MySQLContainer(); // [...] } ``` ## Manually controlling container lifecycle As an alternative, you can manually start the container in a `@BeforeClass`/`@Before` annotated method in your tests. Tear down will be done automatically on JVM exit, but you can of course also use an `@AfterClass`/`@After` annotated method to manually call the `stop()` method on your container. *Example of starting a container in a `@Before` annotated method:* ```java class SimpleMySQLTest { private MySQLContainer mysql = new MySQLContainer(); @Before void before() { mysql.start(); } @After void after() { mysql.stop(); } // [...] } ``` ## Singleton containers Note that the [singleton container pattern](manual_lifecycle_control.md#singleton-containers) is also an option when using JUnit 4. ================================================ FILE: docs/test_framework_integration/junit_5.md ================================================ # Jupiter / JUnit 5 While Testcontainers is tightly coupled with the JUnit 4.x rule API, this module provides an API that is based on the [JUnit Jupiter](https://junit.org/junit5/) extension model. The extension supports two modes: - containers that are restarted for every test method - containers that are shared between all methods of a test class Note that Jupiter/JUnit 5 integration is packaged as a separate library JAR; see [below](#adding-testcontainers-junit-5-support-to-your-project-dependencies) for details. ## Extension Jupiter integration is provided by means of the `@Testcontainers` annotation. The extension finds all fields that are annotated with `@Container` and calls their container lifecycle methods (methods on the `Startable` interface). Containers declared as static fields will be shared between test methods. They will be started only once before any test method is executed and stopped after the last test method has executed. Containers declared as instance fields will be started and stopped for every test method. **Note:** This extension has only been tested with sequential test execution. Using it with parallel test execution is unsupported and may have unintended side effects. *Example:* [Mixed Lifecycle](../../modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/MixedLifecycleTests.java) inside_block:testClass ## Examples To use the Testcontainers extension annotate your test class with `@Testcontainers`. ### Restarted containers To define a restarted container, define an instance field inside your test class and annotate it with the `@Container` annotation. [Restarted Containers](../../modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/TestcontainersNestedRestartedContainerTests.java) inside_block:testClass ### Shared containers Shared containers are defined as static fields in a top level test class and have to be annotated with `@Container`. Note that shared containers can't be declared inside nested test classes. This is because nested test classes have to be defined non-static and can't therefore have static fields. [Shared Container](../../modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/MixedLifecycleTests.java) lines:18-23,32-33,35-36 ## Singleton containers Note that the [singleton container pattern](manual_lifecycle_control.md#singleton-containers) is also an option when using JUnit 5. ## Limitations Since this module has a dependency onto JUnit Jupiter and on Testcontainers core, which has a dependency onto JUnit 4.x, projects using this module will end up with both, JUnit Jupiter and JUnit 4.x in the test classpath. This extension has only been tested with sequential test execution. Using it with parallel test execution is unsupported and may have unintended side effects. ## Adding Testcontainers JUnit 5 support to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-junit-jupiter:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-junit-jupiter {{latest_version}} test ``` ================================================ FILE: docs/test_framework_integration/manual_lifecycle_control.md ================================================ # Manual container lifecycle control While Testcontainers was originally built with JUnit 4 integration in mind, it is fully usable with other test frameworks, or with no framework at all. ## Manually starting/stopping containers Containers can be started and stopped in code using `start()` and `stop()` methods. Additionally, container classes implement `AutoCloseable`. This enables better assurance that the container will be stopped at the appropriate time. ```java try (GenericContainer container = new GenericContainer("imagename")) { container.start(); // ... use the container // no need to call stop() afterwards } ``` ## Singleton containers Sometimes it might be useful to define a container that is only started once for several test classes. There is no special support for this use case provided by the Testcontainers extension. Instead this can be implemented using the following pattern: ```java abstract class AbstractContainerBaseTest { static final MySQLContainer MY_SQL_CONTAINER; static { MY_SQL_CONTAINER = new MySQLContainer(); MY_SQL_CONTAINER.start(); } } class FirstTest extends AbstractContainerBaseTest { @Test void someTestMethod() { String url = MY_SQL_CONTAINER.getJdbcUrl(); // create a connection and run test as normal } } ``` The singleton container is started only once when the base class is loaded. The container can then be used by all inheriting test classes. At the end of the test suite the [Ryuk container](https://github.com/testcontainers/moby-ryuk) that is started by Testcontainers core will take care of stopping the singleton container. ================================================ FILE: docs/test_framework_integration/spock.md ================================================ # Spock [Spock](https://github.com/spockframework/spock) extension for [Testcontainers](https://github.com/testcontainers/testcontainers-java) library, which allows to use Docker containers inside of Spock tests. ## Usage ### `@Testcontainers` class-annotation Specifying the `@Testcontainers` annotation will instruct Spock to start and stop all testcontainers accordingly. This annotation can be mixed with Spock's `@Shared` annotation to indicate, that containers shouldn't be restarted between tests. [PostgresContainerIT](../../modules/spock/src/test/groovy/org/testcontainers/spock/PostgresContainerIT.groovy) inside_block:PostgresContainerIT ## Adding Testcontainers Spock support to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy testImplementation "org.testcontainers:testcontainers-spock:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers testcontainers-spock {{latest_version}} test ``` ## Attributions The initial version of this project was heavily inspired by the excellent [JUnit5 docker extension](https://github.com/FaustXVI/junit5-docker) by [FaustXVI](https://github.com/FaustXVI). ================================================ FILE: docs/theme/main.html ================================================ {% extends "base.html" %} {% block analytics %} {% endblock %} {% block extrahead %} {% endblock %} ================================================ FILE: docs/theme/partials/header.html ================================================ {% set class = "md-header" %} {% if "navigation.tabs.sticky" in features %} {% set class = class ~ " md-header--shadow md-header--lifted" %} {% elif "navigation.tabs" not in features %} {% set class = class ~ " md-header--shadow" %} {% endif %} {% include "partials/tc-header.html" %}
{% if "navigation.tabs.sticky" in features %} {% if "navigation.tabs" in features %} {% include "partials/tabs.html" %} {% endif %} {% endif %}
================================================ FILE: docs/theme/partials/nav.html ================================================ {% set class = "md-nav md-nav--primary" %} {% if "navigation.tabs" in features %} {% set class = class ~ " md-nav--lifted" %} {% endif %} {% if "toc.integrate" in features %} {% set class = class ~ " md-nav--integrated" %} {% endif %} ================================================ FILE: docs/theme/partials/tc-header.html ================================================ {% set header = ({ "siteUrl": "https://testcontainers.com/", "menuItems": [ { "label": "Desktop NEW", "url": "https://testcontainers.com/desktop/" }, { "label": "Cloud", "url": "https://testcontainers.com/cloud/" }, { "label": "Getting Started", "url": "https://testcontainers.com/getting-started/" }, { "label": "Guides", "url": "https://testcontainers.com/guides/" }, { "label": "Modules", "url": "https://testcontainers.com/modules/" }, { "label": "Docs", "children": [ { "label": "Testcontainers for Java", "url": "https://java.testcontainers.org/", "image": "/language-logos/java.svg", }, { "label": "Testcontainers for Go", "url": "https://golang.testcontainers.org/", "image": "/language-logos/go.svg", }, { "label": "Testcontainers for .NET", "url": "https://dotnet.testcontainers.org/", "image": "/language-logos/dotnet.svg", }, { "label": "Testcontainers for Node.js", "url": "https://node.testcontainers.org/", "image": "/language-logos/nodejs.svg", }, { "label": "Testcontainers for Python", "url": "https://testcontainers-python.readthedocs.io/en/latest/", "image": "/language-logos/python.svg", "external": true, }, { "label": "Testcontainers for Rust", "url": "https://docs.rs/testcontainers/latest/testcontainers/", "image": "/language-logos/rust.svg", "external": true, }, { "label": "Testcontainers for Haskell", "url": "https://github.com/testcontainers/testcontainers-hs", "image": "/language-logos/haskell.svg", "external": true, }, { "label": "Testcontainers for Ruby", "url": "https://github.com/testcontainers/testcontainers-ruby", "image": "/language-logos/ruby.svg", "external": true, }, ] }, { "label": "Slack", "url": "https://slack.testcontainers.org/", "icon": "icon-slack", }, { "label": "GitHub", "url": "https://github.com/testcontainers", "icon": "icon-github", }, ] }) %} ================================================ FILE: examples/README.md ================================================ # testcontainers-java-examples > Practical examples of using Testcontainers. The code in this repository accompanies the following blog posts: * [JUnit integration testing with Docker and Testcontainers](https://rnorth.org/junit-integration-testing-with-docker-and-testcontainers) * [Fun with Disque, Java and Spinach](https://rnorth.org/fun-with-disque-java-and-spinach) * [Better JUnit Selenium testing with Docker and Testcontainers](https://rnorth.org/better-junit-selenium-testing-with-docker-and-testcontainers) ================================================ FILE: examples/build.gradle ================================================ // empty build.gradle for dependabot plugins { id 'com.diffplug.spotless' version '6.22.0' apply false } apply from: "$rootDir/../gradle/ci-support.gradle" subprojects { apply plugin:"java" apply from: "$rootDir/../gradle/spotless.gradle" apply plugin: 'checkstyle' repositories { mavenCentral() } test { defaultCharacterEncoding = "UTF-8" testLogging { displayGranularity 1 showStackTraces = true exceptionFormat = 'full' events "STARTED", "PASSED", "FAILED", "SKIPPED" } } checkstyle { toolVersion = "10.23.0" configFile = rootProject.file('../config/checkstyle/checkstyle.xml') } } ================================================ FILE: examples/cucumber/build.gradle ================================================ plugins { id 'java' } repositories { mavenCentral() } dependencies { implementation platform('org.seleniumhq.selenium:selenium-bom:4.35.0') implementation 'org.seleniumhq.selenium:selenium-remote-driver' implementation 'org.seleniumhq.selenium:selenium-firefox-driver' implementation 'org.seleniumhq.selenium:selenium-chrome-driver' testImplementation platform('org.junit:junit-bom:5.13.4') testImplementation 'org.junit.platform:junit-platform-suite' testImplementation platform('io.cucumber:cucumber-bom:7.30.0') testImplementation 'io.cucumber:cucumber-java' testImplementation 'io.cucumber:cucumber-junit-platform-engine' testImplementation 'org.testcontainers:testcontainers-selenium' testImplementation 'org.assertj:assertj-core:3.27.4' testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.3' } test { useJUnitPlatform() } ================================================ FILE: examples/cucumber/src/test/java/org/testcontainers/examples/CucumberTest.java ================================================ package org.testcontainers.examples; import io.cucumber.junit.platform.engine.Constants; import org.junit.platform.suite.api.ConfigurationParameter; import org.junit.platform.suite.api.SelectPackages; import org.junit.platform.suite.api.Suite; @Suite @SelectPackages("org.testcontainers.examples") @ConfigurationParameter(key = Constants.PLUGIN_PROPERTY_NAME, value = "pretty") public class CucumberTest {} ================================================ FILE: examples/cucumber/src/test/java/org/testcontainers/examples/Stepdefs.java ================================================ package org.testcontainers.examples; import io.cucumber.java.After; import io.cucumber.java.Before; import io.cucumber.java.Scenario; import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; import io.cucumber.java.en.When; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; import org.openqa.selenium.chrome.ChromeOptions; import org.openqa.selenium.remote.RemoteWebDriver; import org.testcontainers.containers.BrowserWebDriverContainer; import org.testcontainers.containers.BrowserWebDriverContainer.VncRecordingMode; import org.testcontainers.lifecycle.TestDescription; import java.io.File; import java.util.List; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; public class Stepdefs { private BrowserWebDriverContainer container = new BrowserWebDriverContainer() .withCapabilities(new ChromeOptions()) .withRecordingMode(VncRecordingMode.RECORD_ALL, new File("build")); private String location; private String answer; @Before public void beforeScenario() { container.start(); } @After public void afterScenario(Scenario scenario) { container.afterTest( new TestDescription() { @Override public String getTestId() { return scenario.getId(); } @Override public String getFilesystemFriendlyName() { return scenario.getName(); } }, Optional.of(scenario).filter(Scenario::isFailed).map(__ -> new RuntimeException()) ); } @Given("^location is \"([^\"]*)\"$") public void locationIs(String location) throws Exception { this.location = location; } @When("^I ask is it possible to search here$") public void iAskIsItPossibleToSearchHere() throws Exception { RemoteWebDriver driver = new RemoteWebDriver(container.getSeleniumAddress(), new ChromeOptions()); driver.get(location); List searchInputs = driver.findElements(By.tagName("input")); answer = searchInputs != null && searchInputs.size() > 0 ? "YES" : "NOPE"; } @Then("^I should be told \"([^\"]*)\"$") public void iShouldBeTold(String expected) throws Exception { assertThat(answer).isEqualTo(expected); } } ================================================ FILE: examples/cucumber/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: examples/cucumber/src/test/resources/org/testcontainers/examples/is_search_possible.feature ================================================ Feature: Is it possible to search here? Everybody wants to search something Scenario Outline: Is search possible here Given location is "" When I ask is it possible to search here Then I should be told "" Examples: | location | answer | | https://www.google.com/ | YES | | https://www.google.com/appsstatus | NOPE | ================================================ FILE: examples/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531 distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: examples/gradle.properties ================================================ org.gradle.parallel=false org.gradle.caching=true ================================================ FILE: examples/gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: examples/gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @rem SPDX-License-Identifier: Apache-2.0 @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line set CLASSPATH= @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: examples/hazelcast/build.gradle ================================================ plugins { id 'java' } repositories { mavenCentral() } dependencies { testImplementation 'org.testcontainers:testcontainers' testImplementation 'com.hazelcast:hazelcast:5.3.8' testImplementation 'ch.qos.logback:logback-classic:1.3.15' testImplementation 'org.assertj:assertj-core:3.27.4' testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.11.0' } test { useJUnitPlatform() } ================================================ FILE: examples/hazelcast/src/test/java/org/testcontainers/examples/HazelcastTest.java ================================================ package org.testcontainers.examples; import com.hazelcast.client.HazelcastClient; import com.hazelcast.client.config.ClientConfig; import com.hazelcast.core.HazelcastInstance; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.lifecycle.Startables; import org.testcontainers.utility.DockerImageName; import java.util.concurrent.BlockingQueue; import static org.assertj.core.api.Assertions.assertThat; /** * Examples with Hazelcast using both a single container and a cluster with two containers. */ class HazelcastTest { // Hazelcast values private static final String HZ_IMAGE_NAME = "hazelcast/hazelcast:5.2.0-slim"; private static final String HZ_CLUSTERNAME_ENV_NAME = "HZ_CLUSTERNAME"; private static final String HZ_NETWORK_JOIN_AZURE_ENABLED_ENV_NAME = "HZ_NETWORK_JOIN_AZURE_ENABLED"; private static final String HZ_NETWORK_JOIN_MULTICAST_ENABLED_ENV_NAME = "HZ_NETWORK_JOIN_MULTICAST_ENABLED"; private static final int DEFAULT_EXPOSED_PORT = 5701; // Test values private static final String CLUSTER_STARTUP_LOG_MESSAGE_REGEX = ".*Members \\{size:2.*"; private static final String HOST_PORT_SEPARATOR = ":"; private static final String TEST_QUEUE_NAME = "test-queue"; private static final String TEST_CLUSTER_NAME = "test-cluster"; private static final String TEST_VALUE = "Hello!"; private static final String FALSE_VALUE = "false"; private static final String TRUE_VALUE = "true"; @AfterEach void cleanUp() { HazelcastClient.shutdownAll(); } @Test void singleHazelcastContainer() { try ( GenericContainer container = new GenericContainer<>(DockerImageName.parse(HZ_IMAGE_NAME)) .withExposedPorts(DEFAULT_EXPOSED_PORT) ) { container.start(); assertThat(container.isRunning()).isTrue(); ClientConfig clientConfig = new ClientConfig(); clientConfig .getNetworkConfig() .addAddress(container.getHost() + HOST_PORT_SEPARATOR + container.getFirstMappedPort()); HazelcastInstance client = HazelcastClient.newHazelcastClient(clientConfig); BlockingQueue queue = client.getQueue(TEST_QUEUE_NAME); queue.put(TEST_VALUE); assertThat(queue.take()).isEqualTo(TEST_VALUE); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Interrupted during singleHazelcastContainer test", e); } } @Test void hazelcastCluster() { try ( Network network = Network.newNetwork(); GenericContainer container1 = new GenericContainer<>(DockerImageName.parse(HZ_IMAGE_NAME)) .withExposedPorts(DEFAULT_EXPOSED_PORT) .withEnv(HZ_CLUSTERNAME_ENV_NAME, TEST_CLUSTER_NAME) // Flags necessary to run on Github Actions which runs on an Azure VM .withEnv(HZ_NETWORK_JOIN_AZURE_ENABLED_ENV_NAME, FALSE_VALUE) .withEnv(HZ_NETWORK_JOIN_MULTICAST_ENABLED_ENV_NAME, TRUE_VALUE) .waitingFor(Wait.forLogMessage(CLUSTER_STARTUP_LOG_MESSAGE_REGEX, 1)) .withNetwork(network); GenericContainer container2 = new GenericContainer<>(DockerImageName.parse(HZ_IMAGE_NAME)) .withExposedPorts(DEFAULT_EXPOSED_PORT) .withEnv(HZ_CLUSTERNAME_ENV_NAME, TEST_CLUSTER_NAME) // Flags necessary to run on Github Actions which runs on an Azure VM .withEnv(HZ_NETWORK_JOIN_AZURE_ENABLED_ENV_NAME, FALSE_VALUE) .withEnv(HZ_NETWORK_JOIN_MULTICAST_ENABLED_ENV_NAME, TRUE_VALUE) .waitingFor(Wait.forLogMessage(CLUSTER_STARTUP_LOG_MESSAGE_REGEX, 1)) .withNetwork(network) ) { Startables.deepStart(container1, container2).join(); assertThat(container1.isRunning()).isTrue(); assertThat(container2.isRunning()).isTrue(); ClientConfig clientConfig = new ClientConfig(); clientConfig .setClusterName(TEST_CLUSTER_NAME) .getNetworkConfig() // Uncomment the next line to remove the "WARNING: ...Could not connect to member..." message //.setSmartRouting(false) .addAddress(container1.getHost() + HOST_PORT_SEPARATOR + container1.getFirstMappedPort()) .addAddress(container2.getHost() + HOST_PORT_SEPARATOR + container2.getFirstMappedPort()); HazelcastInstance client = HazelcastClient.newHazelcastClient(clientConfig); assertThat(client.getCluster().getMembers()).hasSize(2); BlockingQueue queue = client.getQueue(TEST_QUEUE_NAME); queue.put(TEST_VALUE); assertThat(queue.take()).isEqualTo(TEST_VALUE); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Interrupted during hazelcastCluster test", e); } } } ================================================ FILE: examples/hazelcast/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: examples/immudb/build.gradle ================================================ plugins { id 'java' } repositories { mavenCentral() } dependencies { implementation 'io.codenotary:immudb4j:1.0.1' testImplementation 'org.testcontainers:testcontainers' testImplementation 'org.testcontainers:testcontainers-junit-jupiter' testImplementation 'org.assertj:assertj-core:3.27.4' testImplementation 'com.google.guava:guava:23.0' testImplementation 'ch.qos.logback:logback-classic:1.3.15' testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.11.0' testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.11.0' } test { useJUnitPlatform() } ================================================ FILE: examples/immudb/src/test/java/ImmuDbTest.java ================================================ import io.codenotary.immudb4j.Entry; import io.codenotary.immudb4j.ImmuClient; import io.codenotary.immudb4j.exceptions.VerificationException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import static org.assertj.core.api.Assertions.assertThat; /** * Test class for the ImmuDbClient. */ @Testcontainers class ImmuDbTest { // Default port for the ImmuDb server private static final int IMMUDB_PORT = 3322; // Default username for the ImmuDb server private final String IMMUDB_USER = "immudb"; // Default password for the ImmuDb server private final String IMMUDB_PASSWORD = "immudb"; // Default database name for the ImmuDb server private final String IMMUDB_DATABASE = "defaultdb"; // Test container for the ImmuDb database, with the latest version of the image and exposed port @Container public static final GenericContainer immuDbContainer = new GenericContainer<>("codenotary/immudb:1.3") .withExposedPorts(IMMUDB_PORT) .waitingFor(Wait.forLogMessage(".*Web API server enabled.*", 1)); // ImmuClient used to interact with the DB private ImmuClient immuClient; @BeforeEach void setUp() { this.immuClient = ImmuClient .newBuilder() .withServerUrl(immuDbContainer.getHost()) .withServerPort(immuDbContainer.getMappedPort(IMMUDB_PORT)) .build(); this.immuClient.openSession(IMMUDB_DATABASE, IMMUDB_USER, IMMUDB_PASSWORD); } @AfterEach void tearDown() { this.immuClient.closeSession(); } @Test void testGetValue() { try { immuClient.set("test1", "test2".getBytes()); Entry entry = immuClient.verifiedGet("test1"); if (entry != null) { byte[] value = entry.getValue(); assertThat(new String(value)).isEqualTo("test2"); } else { Assertions.fail(); } } catch (VerificationException e) { Assertions.fail(); } } } ================================================ FILE: examples/immudb/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: examples/kafka-cluster/build.gradle ================================================ plugins { id 'java' } repositories { mavenCentral() } dependencies { testCompileOnly "org.projectlombok:lombok:1.18.38" testAnnotationProcessor "org.projectlombok:lombok:1.18.38" testImplementation 'org.testcontainers:testcontainers-kafka' testImplementation 'org.apache.kafka:kafka-clients:4.1.0' testImplementation 'org.assertj:assertj-core:3.27.4' testImplementation 'com.google.guava:guava:23.0' testImplementation 'ch.qos.logback:logback-classic:1.3.15' testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' testImplementation 'org.awaitility:awaitility:4.3.0' testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.11.0' } test { useJUnitPlatform() } ================================================ FILE: examples/kafka-cluster/src/test/java/com/example/kafkacluster/ApacheKafkaContainerCluster.java ================================================ package com.example.kafkacluster; import org.apache.kafka.common.Uuid; import org.awaitility.Awaitility; import org.testcontainers.containers.Container; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.kafka.KafkaContainer; import org.testcontainers.lifecycle.Startable; import org.testcontainers.utility.DockerImageName; import java.time.Duration; import java.util.Collection; import java.util.stream.Collectors; import java.util.stream.IntStream; import static org.assertj.core.api.Assertions.assertThat; public class ApacheKafkaContainerCluster implements Startable { private final int brokersNum; private final Network network; private final Collection brokers; public ApacheKafkaContainerCluster(String version, int brokersNum, int internalTopicsRf) { if (brokersNum <= 0) { throw new IllegalArgumentException("brokersNum '" + brokersNum + "' must be greater than 0"); } if (internalTopicsRf <= 0 || internalTopicsRf > brokersNum) { throw new IllegalArgumentException( "internalTopicsRf '" + internalTopicsRf + "' must be less than or equal to brokersNum and greater than 0" ); } this.brokersNum = brokersNum; this.network = Network.newNetwork(); String controllerQuorumVoters = IntStream .range(0, brokersNum) .mapToObj(brokerNum -> String.format("%d@broker-%d:9094", brokerNum, brokerNum)) .collect(Collectors.joining(",")); String clusterId = Uuid.randomUuid().toString(); this.brokers = IntStream .range(0, brokersNum) .mapToObj(brokerNum -> { return new KafkaContainer(DockerImageName.parse("apache/kafka").withTag(version)) .withNetwork(this.network) .withNetworkAliases("broker-" + brokerNum) .withEnv("CLUSTER_ID", clusterId) .withEnv("KAFKA_BROKER_ID", brokerNum + "") .withEnv("KAFKA_NODE_ID", brokerNum + "") .withEnv("KAFKA_CONTROLLER_QUORUM_VOTERS", controllerQuorumVoters) .withEnv("KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR", internalTopicsRf + "") .withEnv("KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS", "0") .withEnv("KAFKA_OFFSETS_TOPIC_NUM_PARTITIONS", internalTopicsRf + "") .withEnv("KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR", internalTopicsRf + "") .withEnv("KAFKA_TRANSACTION_STATE_LOG_MIN_ISR", internalTopicsRf + "") .withStartupTimeout(Duration.ofMinutes(1)); }) .collect(Collectors.toList()); } public Collection getBrokers() { return this.brokers; } public String getBootstrapServers() { return brokers.stream().map(KafkaContainer::getBootstrapServers).collect(Collectors.joining(",")); } @Override public void start() { // Needs to start all the brokers at once brokers.parallelStream().forEach(GenericContainer::start); Awaitility .await() .atMost(Duration.ofSeconds(30)) .untilAsserted(() -> { Container.ExecResult result = this.brokers.stream() .findFirst() .get() .execInContainer( "sh", "-c", "/opt/kafka/bin/kafka-log-dirs.sh --bootstrap-server localhost:9093 --describe | grep -o '\"broker\"' | wc -l" ); String brokers = result.getStdout().replace("\n", ""); assertThat(brokers).asInt().isEqualTo(this.brokersNum); }); } @Override public void stop() { this.brokers.parallelStream().forEach(GenericContainer::stop); } } ================================================ FILE: examples/kafka-cluster/src/test/java/com/example/kafkacluster/ApacheKafkaContainerClusterTest.java ================================================ package com.example.kafkacluster; import com.google.common.collect.ImmutableMap; import org.apache.kafka.clients.admin.AdminClient; import org.apache.kafka.clients.admin.AdminClientConfig; import org.apache.kafka.clients.admin.NewTopic; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecords; import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.common.serialization.StringDeserializer; import org.apache.kafka.common.serialization.StringSerializer; import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; import java.time.Duration; import java.util.Collection; import java.util.Collections; import java.util.UUID; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; class ApacheKafkaContainerClusterTest { @Test void testKafkaContainerCluster() throws Exception { try (ApacheKafkaContainerCluster cluster = new ApacheKafkaContainerCluster("3.8.0", 3, 2)) { cluster.start(); String bootstrapServers = cluster.getBootstrapServers(); assertThat(cluster.getBrokers()).hasSize(3); testKafkaFunctionality(bootstrapServers, 3, 2); } } protected void testKafkaFunctionality(String bootstrapServers, int partitions, int rf) throws Exception { try ( AdminClient adminClient = AdminClient.create( ImmutableMap.of(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers) ); KafkaProducer producer = new KafkaProducer<>( ImmutableMap.of( ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers, ProducerConfig.CLIENT_ID_CONFIG, UUID.randomUUID().toString() ), new StringSerializer(), new StringSerializer() ); KafkaConsumer consumer = new KafkaConsumer<>( ImmutableMap.of( ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers, ConsumerConfig.GROUP_ID_CONFIG, "tc-" + UUID.randomUUID(), ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest" ), new StringDeserializer(), new StringDeserializer() ); ) { String topicName = "messages"; Collection topics = Collections.singletonList(new NewTopic(topicName, partitions, (short) rf)); adminClient.createTopics(topics).all().get(30, TimeUnit.SECONDS); consumer.subscribe(Collections.singletonList(topicName)); producer.send(new ProducerRecord<>(topicName, "testcontainers", "rulezzz")).get(); Awaitility .await() .atMost(Duration.ofSeconds(10)) .untilAsserted(() -> { ConsumerRecords records = consumer.poll(Duration.ofMillis(100)); assertThat(records) .hasSize(1) .extracting(ConsumerRecord::topic, ConsumerRecord::key, ConsumerRecord::value) .containsExactly(tuple(topicName, "testcontainers", "rulezzz")); }); consumer.unsubscribe(); } } } ================================================ FILE: examples/kafka-cluster/src/test/java/com/example/kafkacluster/ConfluentKafkaContainerCluster.java ================================================ package com.example.kafkacluster; import org.apache.kafka.common.Uuid; import org.awaitility.Awaitility; import org.testcontainers.containers.Container; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.kafka.ConfluentKafkaContainer; import org.testcontainers.lifecycle.Startable; import org.testcontainers.utility.DockerImageName; import java.time.Duration; import java.util.Collection; import java.util.stream.Collectors; import java.util.stream.IntStream; import static org.assertj.core.api.Assertions.assertThat; public class ConfluentKafkaContainerCluster implements Startable { private final int brokersNum; private final Network network; private final Collection brokers; public ConfluentKafkaContainerCluster(String confluentPlatformVersion, int brokersNum, int internalTopicsRf) { if (brokersNum <= 0) { throw new IllegalArgumentException("brokersNum '" + brokersNum + "' must be greater than 0"); } if (internalTopicsRf <= 0 || internalTopicsRf > brokersNum) { throw new IllegalArgumentException( "internalTopicsRf '" + internalTopicsRf + "' must be less than or equal to brokersNum and greater than 0" ); } this.brokersNum = brokersNum; this.network = Network.newNetwork(); String controllerQuorumVoters = IntStream .range(0, brokersNum) .mapToObj(brokerNum -> String.format("%d@broker-%d:9094", brokerNum, brokerNum)) .collect(Collectors.joining(",")); String clusterId = Uuid.randomUuid().toString(); this.brokers = IntStream .range(0, brokersNum) .mapToObj(brokerNum -> { return new ConfluentKafkaContainer( DockerImageName.parse("confluentinc/cp-kafka").withTag(confluentPlatformVersion) ) .withNetwork(this.network) .withNetworkAliases("broker-" + brokerNum) .withEnv("CLUSTER_ID", clusterId) .withEnv("KAFKA_BROKER_ID", brokerNum + "") .withEnv("KAFKA_NODE_ID", brokerNum + "") .withEnv("KAFKA_CONTROLLER_QUORUM_VOTERS", controllerQuorumVoters) .withEnv("KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR", internalTopicsRf + "") .withEnv("KAFKA_OFFSETS_TOPIC_NUM_PARTITIONS", internalTopicsRf + "") .withEnv("KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR", internalTopicsRf + "") .withEnv("KAFKA_TRANSACTION_STATE_LOG_MIN_ISR", internalTopicsRf + "") .withStartupTimeout(Duration.ofMinutes(1)); }) .collect(Collectors.toList()); } public Collection getBrokers() { return this.brokers; } public String getBootstrapServers() { return brokers.stream().map(ConfluentKafkaContainer::getBootstrapServers).collect(Collectors.joining(",")); } @Override public void start() { // Needs to start all the brokers at once brokers.parallelStream().forEach(GenericContainer::start); Awaitility .await() .atMost(Duration.ofSeconds(30)) .untilAsserted(() -> { Container.ExecResult result = this.brokers.stream() .findFirst() .get() .execInContainer( "sh", "-c", "kafka-metadata-shell --snapshot /var/lib/kafka/data/__cluster_metadata-0/00000000000000000000.log ls /brokers | wc -l" ); String brokers = result.getStdout().replace("\n", ""); assertThat(brokers).asInt().isEqualTo(this.brokersNum); }); } @Override public void stop() { this.brokers.parallelStream().forEach(GenericContainer::stop); } } ================================================ FILE: examples/kafka-cluster/src/test/java/com/example/kafkacluster/ConfluentKafkaContainerClusterTest.java ================================================ package com.example.kafkacluster; import com.google.common.collect.ImmutableMap; import org.apache.kafka.clients.admin.AdminClient; import org.apache.kafka.clients.admin.AdminClientConfig; import org.apache.kafka.clients.admin.NewTopic; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecords; import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.common.serialization.StringDeserializer; import org.apache.kafka.common.serialization.StringSerializer; import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; import java.time.Duration; import java.util.Collection; import java.util.Collections; import java.util.UUID; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; class ConfluentKafkaContainerClusterTest { @Test void testKafkaContainerCluster() throws Exception { try (ConfluentKafkaContainerCluster cluster = new ConfluentKafkaContainerCluster("7.4.0", 3, 2)) { cluster.start(); String bootstrapServers = cluster.getBootstrapServers(); assertThat(cluster.getBrokers()).hasSize(3); testKafkaFunctionality(bootstrapServers, 3, 2); } } protected void testKafkaFunctionality(String bootstrapServers, int partitions, int rf) throws Exception { try ( AdminClient adminClient = AdminClient.create( ImmutableMap.of(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers) ); KafkaProducer producer = new KafkaProducer<>( ImmutableMap.of( ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers, ProducerConfig.CLIENT_ID_CONFIG, UUID.randomUUID().toString() ), new StringSerializer(), new StringSerializer() ); KafkaConsumer consumer = new KafkaConsumer<>( ImmutableMap.of( ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers, ConsumerConfig.GROUP_ID_CONFIG, "tc-" + UUID.randomUUID(), ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest" ), new StringDeserializer(), new StringDeserializer() ); ) { String topicName = "messages"; Collection topics = Collections.singletonList(new NewTopic(topicName, partitions, (short) rf)); adminClient.createTopics(topics).all().get(30, TimeUnit.SECONDS); consumer.subscribe(Collections.singletonList(topicName)); producer.send(new ProducerRecord<>(topicName, "testcontainers", "rulezzz")).get(); Awaitility .await() .atMost(Duration.ofSeconds(10)) .untilAsserted(() -> { ConsumerRecords records = consumer.poll(Duration.ofMillis(100)); assertThat(records) .hasSize(1) .extracting(ConsumerRecord::topic, ConsumerRecord::key, ConsumerRecord::value) .containsExactly(tuple(topicName, "testcontainers", "rulezzz")); }); consumer.unsubscribe(); } } } ================================================ FILE: examples/kafka-cluster/src/test/java/com/example/kafkacluster/KafkaContainerCluster.java ================================================ package com.example.kafkacluster; import lombok.SneakyThrows; import org.awaitility.Awaitility; import org.testcontainers.containers.Container; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.KafkaContainer; import org.testcontainers.containers.Network; import org.testcontainers.lifecycle.Startable; import org.testcontainers.utility.DockerImageName; import java.time.Duration; import java.util.Collection; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; /** * Provides an easy way to launch a Kafka cluster with multiple brokers. */ public class KafkaContainerCluster implements Startable { private final int brokersNum; private final Network network; private final GenericContainer zookeeper; private final Collection brokers; public KafkaContainerCluster(String confluentPlatformVersion, int brokersNum, int internalTopicsRf) { if (brokersNum <= 0) { throw new IllegalArgumentException("brokersNum '" + brokersNum + "' must be greater than 0"); } if (internalTopicsRf <= 0 || internalTopicsRf > brokersNum) { throw new IllegalArgumentException( "internalTopicsRf '" + internalTopicsRf + "' must be less than or equal to brokersNum and greater than 0" ); } this.brokersNum = brokersNum; this.network = Network.newNetwork(); this.zookeeper = new GenericContainer<>(DockerImageName.parse("confluentinc/cp-zookeeper").withTag(confluentPlatformVersion)) .withNetwork(network) .withNetworkAliases("zookeeper") .withEnv("ZOOKEEPER_CLIENT_PORT", String.valueOf(KafkaContainer.ZOOKEEPER_PORT)); this.brokers = IntStream .range(0, this.brokersNum) .mapToObj(brokerNum -> { return new KafkaContainer( DockerImageName.parse("confluentinc/cp-kafka").withTag(confluentPlatformVersion) ) .withNetwork(this.network) .withNetworkAliases("broker-" + brokerNum) .dependsOn(this.zookeeper) .withExternalZookeeper("zookeeper:" + KafkaContainer.ZOOKEEPER_PORT) .withEnv("KAFKA_BROKER_ID", brokerNum + "") .withEnv("KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR", internalTopicsRf + "") .withEnv("KAFKA_OFFSETS_TOPIC_NUM_PARTITIONS", internalTopicsRf + "") .withEnv("KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR", internalTopicsRf + "") .withEnv("KAFKA_TRANSACTION_STATE_LOG_MIN_ISR", internalTopicsRf + "") .withStartupTimeout(Duration.ofMinutes(1)); }) .collect(Collectors.toList()); } public Collection getBrokers() { return this.brokers; } public String getBootstrapServers() { return brokers.stream().map(KafkaContainer::getBootstrapServers).collect(Collectors.joining(",")); } private Stream> allContainers() { return Stream.concat(this.brokers.stream(), Stream.of(this.zookeeper)); } @Override @SneakyThrows public void start() { // sequential start to avoid resource contention on CI systems with weaker hardware brokers.forEach(GenericContainer::start); Awaitility .await() .atMost(Duration.ofSeconds(30)) .untilAsserted(() -> { Container.ExecResult result = this.zookeeper.execInContainer( "sh", "-c", "zookeeper-shell zookeeper:" + KafkaContainer.ZOOKEEPER_PORT + " ls /brokers/ids | tail -n 1" ); String brokers = result.getStdout(); assertThat(brokers.split(",")).hasSize(this.brokersNum); }); } @Override public void stop() { allContainers().parallel().forEach(GenericContainer::stop); } } ================================================ FILE: examples/kafka-cluster/src/test/java/com/example/kafkacluster/KafkaContainerClusterTest.java ================================================ package com.example.kafkacluster; import com.google.common.collect.ImmutableMap; import org.apache.kafka.clients.admin.AdminClient; import org.apache.kafka.clients.admin.AdminClientConfig; import org.apache.kafka.clients.admin.NewTopic; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecords; import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.common.serialization.StringDeserializer; import org.apache.kafka.common.serialization.StringSerializer; import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; import java.time.Duration; import java.util.Collection; import java.util.Collections; import java.util.UUID; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; class KafkaContainerClusterTest { @Test void testKafkaContainerCluster() throws Exception { try (KafkaContainerCluster cluster = new KafkaContainerCluster("6.2.1", 3, 2)) { cluster.start(); String bootstrapServers = cluster.getBootstrapServers(); assertThat(cluster.getBrokers()).hasSize(3); testKafkaFunctionality(bootstrapServers, 3, 2); } } @Test void testKafkaContainerKraftCluster() throws Exception { try (KafkaContainerKraftCluster cluster = new KafkaContainerKraftCluster("7.0.0", 3, 2)) { cluster.start(); String bootstrapServers = cluster.getBootstrapServers(); assertThat(cluster.getBrokers()).hasSize(3); testKafkaFunctionality(bootstrapServers, 3, 2); } } @Test void testKafkaContainerKraftClusterAfterConfluentPlatform740() throws Exception { try (KafkaContainerKraftCluster cluster = new KafkaContainerKraftCluster("7.4.0", 3, 2)) { cluster.start(); String bootstrapServers = cluster.getBootstrapServers(); assertThat(cluster.getBrokers()).hasSize(3); testKafkaFunctionality(bootstrapServers, 3, 2); } } protected void testKafkaFunctionality(String bootstrapServers, int partitions, int rf) throws Exception { try ( AdminClient adminClient = AdminClient.create( ImmutableMap.of(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers) ); KafkaProducer producer = new KafkaProducer<>( ImmutableMap.of( ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers, ProducerConfig.CLIENT_ID_CONFIG, UUID.randomUUID().toString() ), new StringSerializer(), new StringSerializer() ); KafkaConsumer consumer = new KafkaConsumer<>( ImmutableMap.of( ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers, ConsumerConfig.GROUP_ID_CONFIG, "tc-" + UUID.randomUUID(), ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest" ), new StringDeserializer(), new StringDeserializer() ); ) { String topicName = "messages"; Collection topics = Collections.singletonList(new NewTopic(topicName, partitions, (short) rf)); adminClient.createTopics(topics).all().get(30, TimeUnit.SECONDS); consumer.subscribe(Collections.singletonList(topicName)); producer.send(new ProducerRecord<>(topicName, "testcontainers", "rulezzz")).get(); Awaitility .await() .atMost(Duration.ofSeconds(10)) .untilAsserted(() -> { ConsumerRecords records = consumer.poll(Duration.ofMillis(100)); assertThat(records) .hasSize(1) .extracting(ConsumerRecord::topic, ConsumerRecord::key, ConsumerRecord::value) .containsExactly(tuple(topicName, "testcontainers", "rulezzz")); }); consumer.unsubscribe(); } } } ================================================ FILE: examples/kafka-cluster/src/test/java/com/example/kafkacluster/KafkaContainerKraftCluster.java ================================================ package com.example.kafkacluster; import org.apache.kafka.common.Uuid; import org.awaitility.Awaitility; import org.testcontainers.containers.Container; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.KafkaContainer; import org.testcontainers.containers.Network; import org.testcontainers.lifecycle.Startable; import org.testcontainers.utility.DockerImageName; import java.time.Duration; import java.util.Collection; import java.util.stream.Collectors; import java.util.stream.IntStream; import static org.assertj.core.api.Assertions.assertThat; public class KafkaContainerKraftCluster implements Startable { private final int brokersNum; private final Network network; private final Collection brokers; public KafkaContainerKraftCluster(String confluentPlatformVersion, int brokersNum, int internalTopicsRf) { if (brokersNum <= 0) { throw new IllegalArgumentException("brokersNum '" + brokersNum + "' must be greater than 0"); } if (internalTopicsRf <= 0 || internalTopicsRf > brokersNum) { throw new IllegalArgumentException( "internalTopicsRf '" + internalTopicsRf + "' must be less than or equal to brokersNum and greater than 0" ); } this.brokersNum = brokersNum; this.network = Network.newNetwork(); String controllerQuorumVoters = IntStream .range(0, brokersNum) .mapToObj(brokerNum -> String.format("%d@broker-%d:9094", brokerNum, brokerNum)) .collect(Collectors.joining(",")); String clusterId = Uuid.randomUuid().toString(); this.brokers = IntStream .range(0, brokersNum) .mapToObj(brokerNum -> { return new KafkaContainer( DockerImageName.parse("confluentinc/cp-kafka").withTag(confluentPlatformVersion) ) .withNetwork(this.network) .withNetworkAliases("broker-" + brokerNum) .withKraft() .withClusterId(clusterId) .withEnv("KAFKA_BROKER_ID", brokerNum + "") .withEnv("KAFKA_NODE_ID", brokerNum + "") .withEnv("KAFKA_CONTROLLER_QUORUM_VOTERS", controllerQuorumVoters) .withEnv("KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR", internalTopicsRf + "") .withEnv("KAFKA_OFFSETS_TOPIC_NUM_PARTITIONS", internalTopicsRf + "") .withEnv("KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR", internalTopicsRf + "") .withEnv("KAFKA_TRANSACTION_STATE_LOG_MIN_ISR", internalTopicsRf + "") .withStartupTimeout(Duration.ofMinutes(1)); }) .collect(Collectors.toList()); } public Collection getBrokers() { return this.brokers; } public String getBootstrapServers() { return brokers.stream().map(KafkaContainer::getBootstrapServers).collect(Collectors.joining(",")); } @Override public void start() { // Needs to start all the brokers at once brokers.parallelStream().forEach(GenericContainer::start); Awaitility .await() .atMost(Duration.ofSeconds(30)) .untilAsserted(() -> { Container.ExecResult result = this.brokers.stream() .findFirst() .get() .execInContainer( "sh", "-c", "kafka-metadata-shell --snapshot /var/lib/kafka/data/__cluster_metadata-0/00000000000000000000.log ls /brokers | wc -l" ); String brokers = result.getStdout().replace("\n", ""); assertThat(brokers).asInt().isEqualTo(this.brokersNum); }); } @Override public void stop() { this.brokers.parallelStream().forEach(GenericContainer::stop); } } ================================================ FILE: examples/kafka-cluster/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: examples/nats/build.gradle ================================================ plugins { id 'java' } repositories { mavenCentral() } dependencies { testImplementation 'org.assertj:assertj-core:3.27.4' testImplementation 'org.testcontainers:testcontainers' testImplementation 'io.nats:jnats:2.23.0' testImplementation 'ch.qos.logback:logback-classic:1.3.15' testImplementation 'org.apache.httpcomponents:httpclient:4.5.14' testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.11.0' } test { useJUnitPlatform() } ================================================ FILE: examples/nats/src/test/java/com/example/NatsContainerTest.java ================================================ package com.example; import io.nats.client.Connection; import io.nats.client.Nats; import io.nats.client.Options; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.impl.client.HttpClientBuilder; import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import java.io.IOException; import static org.assertj.core.api.Assertions.assertThat; class NatsContainerTest { public static final Integer NATS_PORT = 4222; public static final Integer NATS_MGMT_PORT = 8222; @Test void test() throws IOException, InterruptedException { try ( GenericContainer nats = new GenericContainer<>("nats:2.9.8-alpine3.16") .withExposedPorts(NATS_PORT, NATS_MGMT_PORT) ) { nats.start(); Connection connection = Nats.connect( new Options.Builder().server("nats://" + nats.getHost() + ":" + nats.getMappedPort(NATS_PORT)).build() ); assertThat(connection.getStatus()).isEqualTo(Connection.Status.CONNECTED); } } @Test void testServerStatus() throws IOException { try ( GenericContainer nats = new GenericContainer<>("nats:2.9.8-alpine3.16") .withExposedPorts(NATS_PORT, NATS_MGMT_PORT) ) { nats.start(); HttpUriRequest request = new HttpGet( String.format("http://%s:%d/varz", nats.getHost(), nats.getMappedPort(NATS_MGMT_PORT)) ); HttpResponse httpResponse = HttpClientBuilder.create().build().execute(request); assertThat(httpResponse.getStatusLine().getStatusCode()).isEqualTo(HttpStatus.SC_OK); } } } ================================================ FILE: examples/nats/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: examples/neo4j-container/build.gradle ================================================ plugins { id 'java' } repositories { mavenCentral() } dependencies { testImplementation 'org.assertj:assertj-core:3.27.4' testImplementation 'org.neo4j.driver:neo4j-java-driver:4.4.20' testImplementation 'org.testcontainers:testcontainers-neo4j' testImplementation 'org.testcontainers:testcontainers-junit-jupiter' testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.11.0' } ================================================ FILE: examples/neo4j-container/src/test/java/org/testcontainers/containers/Neo4jExampleTest.java ================================================ package org.testcontainers.containers; import org.junit.jupiter.api.Test; import org.neo4j.driver.AuthTokens; import org.neo4j.driver.Driver; import org.neo4j.driver.GraphDatabase; import org.neo4j.driver.Session; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Writer; import java.net.HttpURLConnection; import java.net.URL; import java.util.Collections; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; // junitExample { @Testcontainers class Neo4jExampleTest { @Container private static Neo4jContainer neo4jContainer = new Neo4jContainer<>(DockerImageName.parse("neo4j:4.4")) .withoutAuthentication(); // Disable password @Test void testSomethingUsingBolt() { // Retrieve the Bolt URL from the container String boltUrl = neo4jContainer.getBoltUrl(); try (Driver driver = GraphDatabase.driver(boltUrl, AuthTokens.none()); Session session = driver.session()) { long one = session.run("RETURN 1", Collections.emptyMap()).next().get(0).asLong(); assertThat(one).isEqualTo(1L); } catch (Exception e) { fail(e.getMessage()); } } @Test void testSomethingUsingHttp() throws IOException { // Retrieve the HTTP URL from the container String httpUrl = neo4jContainer.getHttpUrl(); URL url = new URL(httpUrl + "/db/data/transaction/commit"); HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.setRequestMethod("POST"); con.setRequestProperty("Content-Type", "application/json"); con.setDoOutput(true); try (Writer out = new OutputStreamWriter(con.getOutputStream())) { out.write("{\"statements\":[{\"statement\":\"RETURN 1\"}]}"); out.flush(); } assertThat(con.getResponseCode()).isEqualTo(HttpURLConnection.HTTP_OK); try (BufferedReader buffer = new BufferedReader(new InputStreamReader(con.getInputStream()))) { String expectedResponse = "{\"results\":[{\"columns\":[\"1\"],\"data\":[{\"row\":[1],\"meta\":[null]}]}],\"errors\":[]}"; String response = buffer.lines().collect(Collectors.joining("\n")); assertThat(response).isEqualTo(expectedResponse); } } } // } ================================================ FILE: examples/neo4j-container/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: examples/ollama-hugging-face/build.gradle ================================================ plugins { id 'java' } repositories { mavenCentral() } dependencies { testImplementation 'org.testcontainers:testcontainers-ollama' testImplementation 'org.assertj:assertj-core:3.27.4' testImplementation 'ch.qos.logback:logback-classic:1.3.15' testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' testImplementation 'io.rest-assured:rest-assured:5.5.6' testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.11.0' } test { useJUnitPlatform() } ================================================ FILE: examples/ollama-hugging-face/src/test/java/com/example/ollamahf/OllamaHuggingFaceContainer.java ================================================ package com.example.ollamahf; import com.github.dockerjava.api.command.InspectContainerResponse; import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.ollama.OllamaContainer; import org.testcontainers.utility.DockerImageName; import java.io.IOException; public class OllamaHuggingFaceContainer extends OllamaContainer { private final HuggingFaceModel huggingFaceModel; public OllamaHuggingFaceContainer(HuggingFaceModel model) { super(DockerImageName.parse("ollama/ollama:0.1.47")); this.huggingFaceModel = model; } @Override protected void containerIsStarted(InspectContainerResponse containerInfo, boolean reused) { super.containerIsStarted(containerInfo, reused); if (reused || huggingFaceModel == null) { return; } try { executeCommand("apt-get", "update"); executeCommand("apt-get", "upgrade", "-y"); executeCommand("apt-get", "install", "-y", "python3-pip"); executeCommand("pip", "install", "huggingface-hub"); executeCommand("hf", "download", huggingFaceModel.repository, huggingFaceModel.model, "--local-dir", "."); executeCommand("sh", "-c", String.format("echo '%s' > Modelfile", huggingFaceModel.modelfileContent)); executeCommand("ollama", "create", huggingFaceModel.model, "-f", "Modelfile"); executeCommand("rm", huggingFaceModel.model); } catch (IOException | InterruptedException e) { throw new ContainerLaunchException(e.getMessage()); } } private void executeCommand(String... command) throws ContainerLaunchException, IOException, InterruptedException { ExecResult execResult = execInContainer(command); if (execResult.getExitCode() > 0) { throw new ContainerLaunchException( "Failed to execute " + String.join(" ", command) + ": " + execResult.getStdout() ); } } public static class HuggingFaceModel { public final String repository; public final String model; public String modelfileContent; public HuggingFaceModel(String repository, String model) { this.repository = repository; this.model = model; this.modelfileContent = "FROM " + model; } } } ================================================ FILE: examples/ollama-hugging-face/src/test/java/com/example/ollamahf/OllamaHuggingFaceTest.java ================================================ package com.example.ollamahf; import io.restassured.http.Header; import org.junit.jupiter.api.Test; import org.testcontainers.ollama.OllamaContainer; import org.testcontainers.utility.DockerImageName; import java.util.List; import static io.restassured.RestAssured.given; import static org.assertj.core.api.Assertions.assertThat; public class OllamaHuggingFaceTest { @Test public void embeddingModelWithHuggingFace() { String repository = "CompendiumLabs/bge-small-en-v1.5-gguf"; String model = "bge-small-en-v1.5-q4_k_m.gguf"; String imageName = "embedding-model-from-hugging-face"; OllamaContainer ollama = new OllamaContainer( DockerImageName.parse(imageName).asCompatibleSubstituteFor("ollama/ollama:0.1.47") ); boolean imageExists = ollama .getDockerClient() .listImagesCmd() .exec() .stream() .anyMatch(image -> image.getRepoTags()[0].equals(imageName + ":latest")); if (!imageExists) { createImage(imageName, repository, model); } ollama.start(); String modelName = given() .baseUri(ollama.getEndpoint()) .get("/api/tags") .jsonPath() .getString("models[0].name"); assertThat(modelName).contains(model + ":latest"); List embedding = given() .baseUri(ollama.getEndpoint()) .header(new Header("Content-Type", "application/json")) .body(new EmbeddingRequest(model + ":latest", "Hello from Testcontainers!")) .post("/api/embeddings") .jsonPath() .getList("embedding"); assertThat(embedding).isNotNull(); assertThat(embedding.isEmpty()).isFalse(); } private static void createImage(String imageName, String repository, String model) { OllamaHuggingFaceContainer.HuggingFaceModel hfModel = new OllamaHuggingFaceContainer.HuggingFaceModel( repository, model ); OllamaHuggingFaceContainer huggingFaceContainer = new OllamaHuggingFaceContainer(hfModel); huggingFaceContainer.start(); huggingFaceContainer.commitToImage(imageName); } public static class EmbeddingRequest { public final String model; public final String prompt; public EmbeddingRequest(String model, String prompt) { this.model = model; this.prompt = prompt; } } } ================================================ FILE: examples/ollama-hugging-face/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: examples/redis-backed-cache/build.gradle ================================================ plugins { id 'java' } repositories { mavenCentral() } dependencies { compileOnly 'org.slf4j:slf4j-api:1.7.36' implementation 'redis.clients:jedis:6.2.0' implementation 'com.google.code.gson:gson:2.13.2' implementation 'com.google.guava:guava:23.0' testImplementation 'org.testcontainers:testcontainers' testImplementation 'org.testcontainers:testcontainers-junit-jupiter' testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' testImplementation 'ch.qos.logback:logback-classic:1.3.15' testImplementation 'org.assertj:assertj-core:3.27.4' testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.11.0' } test { useJUnitPlatform() } ================================================ FILE: examples/redis-backed-cache/src/main/java/com/mycompany/cache/Cache.java ================================================ package com.mycompany.cache; import java.util.Optional; /** * Cache, for storing data associated with keys. */ public interface Cache { /** * Store a value object in the cache with no specific expiry time. The object may be evicted by the cache any time, * if necessary. * * @param key key that may be used to retrieve the object in the future * @param value the value object to be stored */ void put(String key, Object value); /** * Retrieve a value object from the cache. * @param key the key that was used to insert the object initially * @param expectedClass for convenience, a class that the object should be cast to before being returned * @param the class of the returned object * @return the object if it was in the cache, or an empty Optional if not found. */ Optional get(String key, Class expectedClass); } ================================================ FILE: examples/redis-backed-cache/src/main/java/com/mycompany/cache/RedisBackedCache.java ================================================ package com.mycompany.cache; import com.google.gson.Gson; import redis.clients.jedis.Jedis; import java.util.Optional; /** * An implementation of {@link Cache} that stores data in Redis. */ public class RedisBackedCache implements Cache { private final Jedis jedis; private final String cacheName; private final Gson gson; public RedisBackedCache(Jedis jedis, String cacheName) { this.jedis = jedis; this.cacheName = cacheName; this.gson = new Gson(); } @Override public void put(String key, Object value) { String jsonValue = gson.toJson(value); this.jedis.hset(this.cacheName, key, jsonValue); } @Override public Optional get(String key, Class expectedClass) { String foundJson = this.jedis.hget(this.cacheName, key); if (foundJson == null) { return Optional.empty(); } return Optional.of(gson.fromJson(foundJson, expectedClass)); } } ================================================ FILE: examples/redis-backed-cache/src/test/java/RedisBackedCacheTest.java ================================================ import com.mycompany.cache.Cache; import com.mycompany.cache.RedisBackedCache; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; import redis.clients.jedis.Jedis; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; /** * Integration test for Redis-backed cache implementation. */ @Testcontainers class RedisBackedCacheTest { @Container public GenericContainer redis = new GenericContainer<>(DockerImageName.parse("redis:6-alpine")) .withExposedPorts(6379); private Cache cache; @BeforeEach void setUp() throws Exception { Jedis jedis = new Jedis(redis.getHost(), redis.getMappedPort(6379)); cache = new RedisBackedCache(jedis, "test"); } @Test void testFindingAnInsertedValue() { cache.put("foo", "FOO"); Optional foundObject = cache.get("foo", String.class); assertThat(foundObject.isPresent()).as("When an object in the cache is retrieved, it can be found").isTrue(); assertThat(foundObject.get()) .as("When we put a String in to the cache and retrieve it, the value is the same") .isEqualTo("FOO"); } @Test void testNotFindingAValueThatWasNotInserted() { Optional foundObject = cache.get("bar", String.class); assertThat(foundObject.isPresent()) .as("When an object that's not in the cache is retrieved, nothing is found") .isFalse(); } } ================================================ FILE: examples/redis-backed-cache/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: examples/redis-backed-cache-testng/build.gradle ================================================ plugins { id 'java' } repositories { mavenCentral() } dependencies { compileOnly 'org.slf4j:slf4j-api:1.7.36' implementation 'redis.clients:jedis:6.2.0' implementation 'com.google.code.gson:gson:2.13.2' implementation 'com.google.guava:guava:23.0' testImplementation 'org.testcontainers:testcontainers' testImplementation 'ch.qos.logback:logback-classic:1.3.15' testImplementation 'org.testng:testng:7.5.1' testImplementation 'org.assertj:assertj-core:3.27.4' } test { useTestNG() } ================================================ FILE: examples/redis-backed-cache-testng/src/main/java/com/mycompany/cache/Cache.java ================================================ package com.mycompany.cache; import java.util.Optional; /** * Cache, for storing data associated with keys. */ public interface Cache { /** * Store a value object in the cache with no specific expiry time. The object may be evicted by the cache any time, * if necessary. * * @param key key that may be used to retrieve the object in the future * @param value the value object to be stored */ void put(String key, Object value); /** * Retrieve a value object from the cache. * @param key the key that was used to insert the object initially * @param expectedClass for convenience, a class that the object should be cast to before being returned * @param the class of the returned object * @return the object if it was in the cache, or an empty Optional if not found. */ Optional get(String key, Class expectedClass); } ================================================ FILE: examples/redis-backed-cache-testng/src/main/java/com/mycompany/cache/RedisBackedCache.java ================================================ package com.mycompany.cache; import com.google.gson.Gson; import redis.clients.jedis.Jedis; import java.util.Optional; /** * An implementation of {@link Cache} that stores data in Redis. */ public class RedisBackedCache implements Cache { private final Jedis jedis; private final String cacheName; private final Gson gson; public RedisBackedCache(Jedis jedis, String cacheName) { this.jedis = jedis; this.cacheName = cacheName; this.gson = new Gson(); } @Override public void put(String key, Object value) { String jsonValue = gson.toJson(value); this.jedis.hset(this.cacheName, key, jsonValue); } @Override public Optional get(String key, Class expectedClass) { String foundJson = this.jedis.hget(this.cacheName, key); if (foundJson == null) { return Optional.empty(); } return Optional.of(gson.fromJson(foundJson, expectedClass)); } } ================================================ FILE: examples/redis-backed-cache-testng/src/test/java/RedisBackedCacheTest.java ================================================ import com.mycompany.cache.Cache; import com.mycompany.cache.RedisBackedCache; import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.DockerImageName; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import redis.clients.jedis.Jedis; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; /** * Integration test for Redis-backed cache implementation. */ public class RedisBackedCacheTest { private static GenericContainer redis = new GenericContainer<>(DockerImageName.parse("redis:6-alpine")) .withExposedPorts(6379); private Cache cache; @BeforeClass public static void startContainer() { redis.start(); } @AfterClass public static void stopContainer() { redis.stop(); } @BeforeMethod public void setUp() { Jedis jedis = new Jedis(redis.getHost(), redis.getMappedPort(6379)); cache = new RedisBackedCache(jedis, "test"); } @Test public void testFindingAnInsertedValue() { cache.put("foo", "FOO"); Optional foundObject = cache.get("foo", String.class); assertThat(foundObject).as("When an object in the cache is retrieved, it can be found").isPresent(); assertThat(foundObject) .as("When we put a String in to the cache and retrieve it, the value is the same") .contains("FOO"); } @Test public void testNotFindingAValueThatWasNotInserted() { Optional foundObject = cache.get("bar", String.class); assertThat(foundObject) .as("When an object that's not in the cache is retrieved, nothing is found") .isNotPresent(); } } ================================================ FILE: examples/redis-backed-cache-testng/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: examples/selenium-container/build.gradle ================================================ plugins { id 'java' id 'org.springframework.boot' version '3.5.6' } apply plugin: 'io.spring.dependency-management' repositories { mavenCentral() } dependencies { implementation 'org.seleniumhq.selenium:selenium-remote-driver' implementation 'org.seleniumhq.selenium:selenium-firefox-driver' implementation 'org.seleniumhq.selenium:selenium-chrome-driver' implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.testcontainers:testcontainers-selenium' testImplementation 'org.testcontainers:testcontainers-junit-jupiter' testImplementation 'org.assertj:assertj-core:3.27.4' } test { useJUnitPlatform() } ================================================ FILE: examples/selenium-container/src/main/java/com/example/DemoApplication.java ================================================ package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } } ================================================ FILE: examples/selenium-container/src/main/resources/static/foo.html ================================================ Getting Started: Serving Web Content Hello World ================================================ FILE: examples/selenium-container/src/test/java/SeleniumContainerTest.java ================================================ import com.example.DemoApplication; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; import org.openqa.selenium.chrome.ChromeOptions; import org.openqa.selenium.remote.RemoteWebDriver; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.web.context.WebServerInitializedEvent; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.testcontainers.Testcontainers; import org.testcontainers.containers.BrowserWebDriverContainer; import org.testcontainers.containers.BrowserWebDriverContainer.VncRecordingMode; import org.testcontainers.junit.jupiter.Container; import java.io.File; import java.time.Duration; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; /** * Simple example of plain Selenium usage. */ @org.testcontainers.junit.jupiter.Testcontainers @ExtendWith(SpringExtension.class) @SpringBootTest(classes = DemoApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ContextConfiguration(initializers = SeleniumContainerTest.Initializer.class) class SeleniumContainerTest { @LocalServerPort private int port; @Container public BrowserWebDriverContainer chrome = new BrowserWebDriverContainer() .withCapabilities(new ChromeOptions()) .withRecordingMode(VncRecordingMode.RECORD_ALL, new File("build")); @Test void simplePlainSeleniumTest() { RemoteWebDriver driver = new RemoteWebDriver(chrome.getSeleniumAddress(), new ChromeOptions()); driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(30)); driver.get("http://host.testcontainers.internal:" + port + "/foo.html"); List hElement = driver.findElements(By.tagName("h")); assertThat(hElement).as("The h element is found").isNotEmpty(); } static class Initializer implements ApplicationContextInitializer { @Override public void initialize(ConfigurableApplicationContext applicationContext) { applicationContext.addApplicationListener( (ApplicationListener) event -> { Testcontainers.exposeHostPorts(event.getWebServer().getPort()); } ); } } } ================================================ FILE: examples/selenium-container/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: examples/settings.gradle ================================================ buildscript { repositories { maven { url "https://plugins.gradle.org/m2/" } } dependencies { classpath "gradle.plugin.ch.myniva.gradle:s3-build-cache:0.10.0" classpath "com.gradle.enterprise:com.gradle.enterprise.gradle.plugin:3.17.4" classpath "com.gradle:common-custom-user-data-gradle-plugin:2.0.1" } } apply plugin: 'com.gradle.develocity' apply plugin: "com.gradle.common-custom-user-data-gradle-plugin" rootProject.name = 'testcontainers-examples' includeBuild '..' // explicit include to allow Dependabot to autodiscover subprojects include 'kafka-cluster' include 'neo4j-container' include 'redis-backed-cache' include 'redis-backed-cache-testng' include 'selenium-container' include 'singleton-container' include 'solr-container' include 'spring-boot' include 'cucumber' include 'spring-boot-kotlin-redis' include 'immudb' include 'zookeeper' include 'hazelcast' include 'nats' include 'sftp' include 'ollama-hugging-face' ext.isCI = System.getenv("CI") != null buildCache { local { enabled = !isCI } remote(develocity.buildCache) { push = isCI && !System.getenv("READ_ONLY_REMOTE_GRADLE_CACHE") && System.getenv("DEVELOCITY_ACCESS_KEY") enabled = true } } develocity { buildScan { server = "https://ge.testcontainers.org/" publishing.onlyIf { it.authenticated } uploadInBackground = !isCI capture.fileFingerprints = true } } ================================================ FILE: examples/sftp/build.gradle ================================================ plugins { id 'java' } repositories { mavenCentral() } dependencies { testImplementation 'com.github.mwiede:jsch:2.27.2' testImplementation 'org.testcontainers:testcontainers' testImplementation 'org.assertj:assertj-core:3.27.4' testImplementation 'ch.qos.logback:logback-classic:1.3.15' testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.11.0' } test { useJUnitPlatform() } ================================================ FILE: examples/sftp/src/test/java/org/example/SftpContainerTest.java ================================================ package org.example; import com.jcraft.jsch.ChannelSftp; import com.jcraft.jsch.HostKey; import com.jcraft.jsch.JSch; import com.jcraft.jsch.Session; import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.MountableFile; import java.io.BufferedReader; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; class SftpContainerTest { @Test void test() throws Exception { try ( GenericContainer sftp = new GenericContainer<>("atmoz/sftp:alpine-3.7") .withCopyFileToContainer( MountableFile.forClasspathResource("testcontainers/", 0777), "/home/foo/upload/testcontainers" ) .withExposedPorts(22) .withCommand("foo:pass:::upload") ) { sftp.start(); JSch jsch = new JSch(); Session jschSession = jsch.getSession("foo", sftp.getHost(), sftp.getMappedPort(22)); jschSession.setPassword("pass"); jschSession.setConfig("StrictHostKeyChecking", "no"); jschSession.connect(); ChannelSftp channel = (ChannelSftp) jschSession.openChannel("sftp"); channel.connect(); assertThat(channel.ls("/upload/testcontainers")).anyMatch(item -> item.toString().contains("file.txt")); assertThat( new BufferedReader( new InputStreamReader(channel.get("/upload/testcontainers/file.txt"), StandardCharsets.UTF_8) ) .lines() .collect(Collectors.joining("\n")) ) .contains("Testcontainers"); channel.rm("/upload/testcontainers/file.txt"); assertThat(channel.ls("/upload/testcontainers/")) .noneMatch(item -> item.toString().contains("testcontainers/file.txt")); } } @Test void testHostKeyCheck() throws Exception { try ( GenericContainer sftp = new GenericContainer<>("atmoz/sftp:alpine-3.7") .withCopyFileToContainer( MountableFile.forClasspathResource("testcontainers/", 0777), "/home/foo/upload/testcontainers" ) .withCopyFileToContainer( MountableFile.forClasspathResource("./ssh_host_rsa_key", 0400), "/etc/ssh/ssh_host_rsa_key" ) .withExposedPorts(22) .withCommand("foo:pass:::upload") ) { sftp.start(); JSch jsch = new JSch(); Session jschSession = jsch.getSession("foo", sftp.getHost(), sftp.getMappedPort(22)); jschSession.setPassword("pass"); // hostKeyString is string starting with AAAA from file known_hosts or ssh_host_*_key.pub // generate the files with: // ssh-keygen -t rsa -b 3072 -f ssh_host_rsa_key < /dev/null String hostKeyString = "AAAAB3NzaC1yc2EAAAADAQABAAABgQCXMxVRzmFWxfrRB9XiZ/3HNM+xkYYE+IMGuOZD" + "04M2ezU25XjT6cPajzpFmzTxR2qEpRCKHeVnSG5nT6UXQp7760brTN7m5sDasbMnHgYh" + "fC/3of2k6qTR9X/JHRpgwzq5+6FtEe41w1H1dXoNIr4YTKnLijSp8MKqBtPPNUpzEVb9" + "5YKZGdCDoCbbYOyS/Dc8azUDo0mqM542J3nA2Sq9HCP0BAv43hrTAtCZodkB5wo18exb" + "fPKsjGtA3de2npybFoSRbavZmT8L/b2iHZX6FRaqLsbYGKtszCWu5OU7WBX5g5QVlLfO" + "nGQ+LsF6d6pX5LlMwEU14uu4gNPvZFOaZXtHNHZqnBcjd/sMaw5N/atFsPgtQ0vYnrEA" + "D6oDjj0uXMsnmgUWTZBi3q2GBWWPqhE+0ASb2xBQGa+tWWTVYbuuYlA7hUX0URK8FcLw" + "4UOYJjscDjnjlvQkghd2esP5NxV1NXkG2XYNHnf1E/tH4+AHJzy+qOQom7ehda96FZ8="; HostKey hostKey = new HostKey(sftp.getHost(), Base64.getDecoder().decode(hostKeyString)); jschSession.getHostKeyRepository().add(hostKey, null); jschSession.connect(); ChannelSftp channel = (ChannelSftp) jschSession.openChannel("sftp"); channel.connect(); assertThat(channel.ls("/upload/testcontainers")).anyMatch(item -> item.toString().contains("file.txt")); assertThat( new BufferedReader( new InputStreamReader(channel.get("/upload/testcontainers/file.txt"), StandardCharsets.UTF_8) ) .lines() .collect(Collectors.joining("\n")) ) .contains("Testcontainers"); channel.rm("/upload/testcontainers/file.txt"); assertThat(channel.ls("/upload/testcontainers/")) .noneMatch(item -> item.toString().contains("testcontainers/file.txt")); } } } ================================================ FILE: examples/sftp/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: examples/sftp/src/test/resources/ssh_host_rsa_key ================================================ -----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn NhAAAAAwEAAQAAAYEAlzMVUc5hVsX60QfV4mf9xzTPsZGGBPiDBrjmQ9ODNns1NuV40+nD 2o86RZs08UdqhKUQih3lZ0huZ0+lF0Ke++tG60ze5ubA2rGzJx4GIXwv96H9pOqk0fV/yR 0aYMM6ufuhbRHuNcNR9XV6DSK+GEypy4o0qfDCqgbTzzVKcxFW/eWCmRnQg6Am22Dskvw3 PGs1A6NJqjOeNid5wNkqvRwj9AQL+N4a0wLQmaHZAecKNfHsW3zyrIxrQN3Xtp6cmxaEkW 2r2Zk/C/29oh2V+hUWqi7G2BirbMwlruTlO1gV+YOUFZS3zpxkPi7BeneqV+S5TMBFNeLr uIDT72RTmmV7RzR2apwXI3f7DGsOTf2rRbD4LUNL2J6xAA+qA449LlzLJ5oFFk2QYt6thg Vlj6oRPtAEm9sQUBmvrVlk1WG7rmJQO4VF9FESvBXC8OFDmCY7HA4545b0JIIXdnrD+TcV dTV5Btl2DR539RP7R+PgByc8vqjkKJu3oXWvehWfAAAFiPUCzjT1As40AAAAB3NzaC1yc2 EAAAGBAJczFVHOYVbF+tEH1eJn/cc0z7GRhgT4gwa45kPTgzZ7NTbleNPpw9qPOkWbNPFH aoSlEIod5WdIbmdPpRdCnvvrRutM3ubmwNqxsyceBiF8L/eh/aTqpNH1f8kdGmDDOrn7oW 0R7jXDUfV1eg0ivhhMqcuKNKnwwqoG0881SnMRVv3lgpkZ0IOgJttg7JL8NzxrNQOjSaoz njYnecDZKr0cI/QEC/jeGtMC0Jmh2QHnCjXx7Ft88qyMa0Dd17aenJsWhJFtq9mZPwv9va IdlfoVFqouxtgYq2zMJa7k5TtYFfmDlBWUt86cZD4uwXp3qlfkuUzARTXi67iA0+9kU5pl e0c0dmqcFyN3+wxrDk39q0Ww+C1DS9iesQAPqgOOPS5cyyeaBRZNkGLerYYFZY+qET7QBJ vbEFAZr61ZZNVhu65iUDuFRfRRErwVwvDhQ5gmOxwOOeOW9CSCF3Z6w/k3FXU1eQbZdg0e d/UT+0fj4AcnPL6o5Cibt6F1r3oVnwAAAAMBAAEAAAGALcv8wKcUx6423tqTN70M2qpN4H h2Egpd0YruwAuQWk+uWh7eXr2XI5uvaEbvHcfmZSAEJvmQMxz2x9cRZ763nhFxDTNe7qxl LLiXTZlj/P97HfQUej/SRYApQPbONxHbN1sW1Y0RTHqJWCJJojHsRzrtUSfe9Lxmkg54WH JJRxow8b1zNcFibYP0UQ2GCq1XY7cLOztZxDJXUQra74U300jzQOV65NoNYO2g1m/15YQg DR/mWf26GXZ8xAyN2pQm3wiI86kY1UP+2kVr38tGcJ+Xrm08Pav06IiEUdFAdDRLL0AWXY ZG25BBJn2VaPZoE5+MH7xRQ2BrqNUZ6ec8jTPZXWN6VyZCmn06KRblIRnv/NcMV5GH/lE9 JbP/MnQQzsQAO0REfhcrdb66I6l0jMTwQcvSJyPXLVl1UvobzcF+CpcExsoaQj5U9cwhkG XRLqPhI76+L0L2kNefQ4yN5MhxWiajKUOknRITkvmNR+jJYsUN/ziODRevbakBzyqtAAAA wCpC6P+iJg19HdhNf6I2IUQErPoltUhA5bsUGmuseCn19Y3V5RmNa8+HHfbnMkUSoFzTvS j0l7rkxl0vvPmz0zr/2ehWiMbReFRy3hGl55AGPLE7pjIy08JIUcQm2jH8C3oeSKNwCrYV +HWsOsQu4+/uOTgp6I46+iSLLG+xjH+5zLtvxa6+o+zLjAOSW4aweAw1WAXy8J4ylAv2nA n3g3Rfa7C0qZG1bZ63phcgv2BNzN+QgmORoh5v5ICvT+qJ5wAAAMEAwvdI3XsLV0uzNkAq C9aWyK4cAdphvCb8n0oz5Vrm6j/qFRXzcDZLtkMboCRE2qVqNLQjMiTJo/QjX9jxe7LD6c Vxtlcl2Ts8qrixFhKXJNwC/lq/TTe2dpMSYm61OINK3TiofZi6eff/ubcpq7zr3iVyWk5b wAVSun8q+Su7ziYYb+MuBQsKn5VWyoYK+E/LFItY26ulOxbrntB805JsXpjbYrL0KoXJCx 6ZWdBVsvbD733WipNbPQZ+4JYDbun7AAAAwQDGiFOALlS5nidWFqMeMm/dGsHpwri0b10Z Bf/DPPxK6EuFKLUppt6KMl2zJjwVa2NqSTppz7TpUP6jC5pSglxtcvatEIRVF8KBxuIJ/G 8Wav3Xuxu9nrRyKAzXjrjU+4TjAH1jBfTj3/tDdRagxt7JESirE+sYW5nie9XpzW4ehsf6 fJacmwoiGdSCc4dldD8ZkEXcmCChFTH+PY3uYtiJr+znzbUZ1RLL3Uk2xHWOWSHz/1tUBy BFP58e3rYvNa0AAAAPYWFAMjMtMDcxNTMtMDA5AQIDBA== -----END OPENSSH PRIVATE KEY----- ================================================ FILE: examples/sftp/src/test/resources/ssh_host_rsa_key.pub ================================================ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCXMxVRzmFWxfrRB9XiZ/3HNM+xkYYE+IMGuOZD04M2ezU25XjT6cPajzpFmzTxR2qEpRCKHeVnSG5nT6UXQp7760brTN7m5sDasbMnHgYhfC/3of2k6qTR9X/JHRpgwzq5+6FtEe41w1H1dXoNIr4YTKnLijSp8MKqBtPPNUpzEVb95YKZGdCDoCbbYOyS/Dc8azUDo0mqM542J3nA2Sq9HCP0BAv43hrTAtCZodkB5wo18exbfPKsjGtA3de2npybFoSRbavZmT8L/b2iHZX6FRaqLsbYGKtszCWu5OU7WBX5g5QVlLfOnGQ+LsF6d6pX5LlMwEU14uu4gNPvZFOaZXtHNHZqnBcjd/sMaw5N/atFsPgtQ0vYnrEAD6oDjj0uXMsnmgUWTZBi3q2GBWWPqhE+0ASb2xBQGa+tWWTVYbuuYlA7hUX0URK8FcLw4UOYJjscDjnjlvQkghd2esP5NxV1NXkG2XYNHnf1E/tH4+AHJzy+qOQom7ehda96FZ8= someone@localhost ================================================ FILE: examples/sftp/src/test/resources/testcontainers/file.txt ================================================ Testcontainers ================================================ FILE: examples/singleton-container/build.gradle ================================================ plugins { id 'java' } repositories { mavenCentral() } dependencies { implementation 'redis.clients:jedis:6.2.0' implementation 'com.google.code.gson:gson:2.13.2' implementation 'com.google.guava:guava:23.0' compileOnly 'org.slf4j:slf4j-api:1.7.36' testImplementation 'ch.qos.logback:logback-classic:1.3.15' testImplementation 'org.testcontainers:testcontainers' testImplementation 'org.assertj:assertj-core:3.27.4' testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.11.0' } test { useJUnitPlatform() } ================================================ FILE: examples/singleton-container/src/main/java/com/example/cache/Cache.java ================================================ package com.example.cache; import java.util.Optional; public interface Cache { void put(String key, Object value); Optional get(String key, Class expectedClass); } ================================================ FILE: examples/singleton-container/src/main/java/com/example/cache/RedisBackedCache.java ================================================ package com.example.cache; import com.google.gson.Gson; import redis.clients.jedis.Jedis; import java.util.Optional; public class RedisBackedCache implements Cache { private final Jedis jedis; private final String cacheName; private final Gson gson; public RedisBackedCache(Jedis jedis, String cacheName) { this.jedis = jedis; this.cacheName = cacheName; this.gson = new Gson(); } public void put(String key, Object value) { String jsonValue = gson.toJson(value); this.jedis.hset(this.cacheName, key, jsonValue); } public Optional get(String key, Class expectedClass) { String foundJson = this.jedis.hget(this.cacheName, key); if (foundJson == null) { return Optional.empty(); } return Optional.of(gson.fromJson(foundJson, expectedClass)); } } ================================================ FILE: examples/singleton-container/src/test/java/com/example/AbstractIntegrationTest.java ================================================ package com.example; import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.DockerImageName; public abstract class AbstractIntegrationTest { public static final GenericContainer redis = new GenericContainer<>(DockerImageName.parse("redis:6-alpine")) .withExposedPorts(6379); static { redis.start(); } } ================================================ FILE: examples/singleton-container/src/test/java/com/example/BarConcreteTestClass.java ================================================ package com.example; import com.example.cache.Cache; import com.example.cache.RedisBackedCache; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import redis.clients.jedis.Jedis; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; class BarConcreteTestClass extends AbstractIntegrationTest { private Cache cache; @BeforeEach void setUp() { Jedis jedis = new Jedis(redis.getHost(), redis.getMappedPort(6379)); cache = new RedisBackedCache(jedis, "bar"); } @Test void testInsertValue() { cache.put("bar", "BAR"); Optional foundObject = cache.get("bar", String.class); assertThat(foundObject).as("When inserting an object into the cache, it can be retrieved").isPresent(); assertThat(foundObject) .as("When accessing the value of a retrieved object, the value must be the same") .contains("BAR"); } } ================================================ FILE: examples/singleton-container/src/test/java/com/example/FooConcreteTestClass.java ================================================ package com.example; import com.example.cache.Cache; import com.example.cache.RedisBackedCache; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import redis.clients.jedis.Jedis; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; class FooConcreteTestClass extends AbstractIntegrationTest { private Cache cache; @BeforeEach void setUp() { Jedis jedis = new Jedis(redis.getHost(), redis.getMappedPort(6379)); cache = new RedisBackedCache(jedis, "foo"); } @Test void testInsertValue() { cache.put("foo", "FOO"); Optional foundObject = cache.get("foo", String.class); assertThat(foundObject).as("When inserting an object into the cache, it can be retrieved").isPresent(); assertThat(foundObject) .as("When accessing the value of a retrieved object, the value must be the same") .contains("FOO"); } } ================================================ FILE: examples/singleton-container/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: examples/solr-container/build.gradle ================================================ plugins { id 'java' } repositories { mavenCentral() } dependencies { compileOnly "org.projectlombok:lombok:1.18.38" annotationProcessor "org.projectlombok:lombok:1.18.38" implementation 'org.apache.solr:solr-solrj:8.11.4' testImplementation 'org.testcontainers:testcontainers' testImplementation 'org.testcontainers:testcontainers-solr' testImplementation 'org.assertj:assertj-core:3.27.4' testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.11.0' } test { useJUnitPlatform() } ================================================ FILE: examples/solr-container/src/main/java/com/example/SearchEngine.java ================================================ package com.example; public interface SearchEngine { public SearchResult search(String term); } ================================================ FILE: examples/solr-container/src/main/java/com/example/SearchResult.java ================================================ package com.example; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; import java.util.Map; @Data @Builder @NoArgsConstructor @AllArgsConstructor public class SearchResult { private long totalHits; private List> results; } ================================================ FILE: examples/solr-container/src/main/java/com/example/SolrSearchEngine.java ================================================ package com.example; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.client.solrj.util.ClientUtils; import org.apache.solr.common.SolrDocument; import java.util.stream.Collectors; @RequiredArgsConstructor public class SolrSearchEngine implements SearchEngine { public static final String COLLECTION_NAME = "products"; private final SolrClient client; @SneakyThrows public SearchResult search(String term) { SolrQuery query = new SolrQuery(); query.setQuery("title:" + ClientUtils.escapeQueryChars(term)); QueryResponse response = client.query(COLLECTION_NAME, query); return createResult(response); } private SearchResult createResult(QueryResponse response) { return SearchResult .builder() .totalHits(response.getResults().getNumFound()) .results(response.getResults().stream().map(SolrDocument::getFieldValueMap).collect(Collectors.toList())) .build(); } } ================================================ FILE: examples/solr-container/src/test/java/com/example/SolrQueryTest.java ================================================ package com.example; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.impl.Http2SolrClient; import org.apache.solr.common.SolrInputDocument; import org.apache.solr.common.SolrInputField; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.testcontainers.containers.SolrContainer; import org.testcontainers.utility.DockerImageName; import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; class SolrQueryTest { private static final DockerImageName SOLR_IMAGE = DockerImageName.parse("solr:8.3.0"); public static final SolrContainer solrContainer = new SolrContainer(SOLR_IMAGE) .withCollection(SolrSearchEngine.COLLECTION_NAME); private static SolrClient solrClient; @BeforeAll static void setUp() throws IOException, SolrServerException { solrContainer.start(); solrClient = new Http2SolrClient.Builder( "http://" + solrContainer.getHost() + ":" + solrContainer.getSolrPort() + "/solr" ) .build(); // Add Sample Data solrClient.add( SolrSearchEngine.COLLECTION_NAME, Collections.singletonList( new SolrInputDocument( createMap( "id", createInputField("id", "1"), "title", createInputField("title", "old skool - trainers - shoes") ) ) ) ); solrClient.add( SolrSearchEngine.COLLECTION_NAME, Collections.singletonList( new SolrInputDocument( createMap("id", createInputField("id", "2"), "title", createInputField("title", "print t-shirt")) ) ) ); solrClient.commit(SolrSearchEngine.COLLECTION_NAME); } @Test void testQueryForShoes() { SolrSearchEngine searchEngine = new SolrSearchEngine(solrClient); SearchResult result = searchEngine.search("shoes"); assertThat(result.getTotalHits()).as("When searching for shoes we expect one result").isEqualTo(1L); assertThat(result.getResults().get(0).get("id")).as("The result should have the id 1").isEqualTo("1"); } @Test void testQueryForTShirt() { SolrSearchEngine searchEngine = new SolrSearchEngine(solrClient); SearchResult result = searchEngine.search("t-shirt"); assertThat(result.getTotalHits()).as("When searching for t-shirt we expect one result").isEqualTo(1L); assertThat(result.getResults().get(0).get("id")).as("The result should have the id 2").isEqualTo("2"); } @Test void testQueryForAsterisk() { SolrSearchEngine searchEngine = new SolrSearchEngine(solrClient); SearchResult result = searchEngine.search("*"); assertThat(result.getTotalHits()).as("When searching for * we expect no results").isEqualTo(0L); } private static SolrInputField createInputField(String key, String value) { SolrInputField inputField = new SolrInputField(key); inputField.setValue(value); return inputField; } private static Map createMap(String k0, SolrInputField v0, String k1, SolrInputField v1) { Map result = new HashMap<>(); result.put(k0, v0); result.put(k1, v1); return result; } } ================================================ FILE: examples/solr-container/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: examples/spring-boot/build.gradle ================================================ plugins { id 'java' id 'org.springframework.boot' version '2.7.18' } apply plugin: 'io.spring.dependency-management' repositories { mavenCentral() } dependencies { compileOnly "org.projectlombok:lombok" annotationProcessor "org.projectlombok:lombok" implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-web' runtimeOnly 'org.postgresql:postgresql' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.testcontainers:testcontainers-postgresql' testRuntimeOnly "org.junit.platform:junit-platform-launcher:1.8.2" } test { useJUnitPlatform() } ================================================ FILE: examples/spring-boot/src/main/java/com/example/DemoApplication.java ================================================ package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; @SpringBootApplication @EnableJpaRepositories public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } } ================================================ FILE: examples/spring-boot/src/main/java/com/example/DemoController.java ================================================ package com.example; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RestController public class DemoController { private final StringRedisTemplate stringRedisTemplate; private final DemoService demoService; public DemoController(StringRedisTemplate stringRedisTemplate, DemoService demoService) { this.stringRedisTemplate = stringRedisTemplate; this.demoService = demoService; } @GetMapping("/foo") public String get() { return stringRedisTemplate.opsForValue().get("foo"); } @PutMapping("/foo") public void set(@RequestBody String value) { stringRedisTemplate.opsForValue().set("foo", value); } @GetMapping("/{id}") public DemoEntity getDemoEntity(@PathVariable("id") Long id) { return demoService.getDemoEntity(id); } } ================================================ FILE: examples/spring-boot/src/main/java/com/example/DemoEntity.java ================================================ package com.example; import lombok.Data; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Data @Entity public class DemoEntity { @Id @GeneratedValue private Long id; @Column private String value; } ================================================ FILE: examples/spring-boot/src/main/java/com/example/DemoRepository.java ================================================ package com.example; import org.springframework.data.jpa.repository.JpaRepository; public interface DemoRepository extends JpaRepository {} ================================================ FILE: examples/spring-boot/src/main/java/com/example/DemoService.java ================================================ package com.example; import org.springframework.stereotype.Service; @Service public class DemoService { private final DemoRepository demoRepository; public DemoService(DemoRepository demoRepository) { this.demoRepository = demoRepository; } public DemoEntity getDemoEntity(Long id) { return demoRepository.findById(id).orElseThrow(() -> new RuntimeException("Entity not found")); } } ================================================ FILE: examples/spring-boot/src/main/resources/application.yml ================================================ spring: jpa: hibernate: ddl-auto: create show-sql: true properties: hibernate: jdbc: lob: non_contextual_creation: true ================================================ FILE: examples/spring-boot/src/test/java/com/example/AbstractIntegrationTest.java ================================================ package com.example; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.DockerImageName; @SpringBootTest( classes = DemoApplication.class, webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "spring.datasource.url=jdbc:tc:postgresql:11-alpine:///databasename" } ) @ActiveProfiles("test") abstract class AbstractIntegrationTest { static GenericContainer redis = new GenericContainer<>(DockerImageName.parse("redis:6-alpine")) .withExposedPorts(6379); @DynamicPropertySource static void redisProperties(DynamicPropertyRegistry registry) { redis.start(); registry.add("spring.redis.host", redis::getHost); registry.add("spring.redis.port", redis::getFirstMappedPort); } } ================================================ FILE: examples/spring-boot/src/test/java/com/example/DemoControllerTest.java ================================================ package com.example; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.web.client.TestRestTemplate; import static org.assertj.core.api.Assertions.assertThat; class DemoControllerTest extends AbstractIntegrationTest { @Autowired TestRestTemplate restTemplate; @Autowired DemoRepository demoRepository; @Test void simpleTest() { String fooResource = "/foo"; restTemplate.put(fooResource, "bar"); assertThat(restTemplate.getForObject(fooResource, String.class)).as("value is set").isEqualTo("bar"); } @Test void simpleJPATest() { DemoEntity demoEntity = new DemoEntity(); demoEntity.setValue("Some value"); demoRepository.save(demoEntity); DemoEntity result = restTemplate.getForObject("/" + demoEntity.getId(), DemoEntity.class); assertThat(result.getValue()).as("value is set").isEqualTo("Some value"); } } ================================================ FILE: examples/spring-boot/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: examples/spring-boot-kotlin-redis/build.gradle.kts ================================================ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.springframework.boot") version "2.7.18" kotlin("jvm") version "1.8.22" kotlin("plugin.spring") version "1.8.22" } java.sourceCompatibility = JavaVersion.VERSION_1_8 repositories { mavenCentral() } dependencies { apply(plugin = "io.spring.dependency-management") implementation("org.springframework.boot:spring-boot-starter") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("org.springframework.boot:spring-boot-starter-data-redis") implementation("org.springframework.boot:spring-boot-starter-web") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.testcontainers:testcontainers") testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.8.2") } tasks.withType { kotlinOptions { freeCompilerArgs = listOf("-Xjsr305=strict") jvmTarget = "1.8" } } tasks.withType { useJUnitPlatform() } ================================================ FILE: examples/spring-boot-kotlin-redis/src/main/kotlin/com/example/redis/ExampleController.kt ================================================ package com.example.redis import org.springframework.data.redis.core.RedisTemplate import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RestController @RestController class ExampleController( private val redisTemplate: RedisTemplate ) { companion object { const val key = "testcontainers" } @PostMapping("/set-foo") fun setFoo(@RequestBody value: String) { redisTemplate.opsForValue().set(key, value) } @GetMapping("/get-foo") fun getFoo(): String? { return redisTemplate.opsForValue().get(key) } } ================================================ FILE: examples/spring-boot-kotlin-redis/src/main/kotlin/com/example/redis/RedisApplication.kt ================================================ package com.example.redis import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @SpringBootApplication class RedisApplication fun main(args: Array) { runApplication(*args) } ================================================ FILE: examples/spring-boot-kotlin-redis/src/test/kotlin/com/example/redis/AbstractIntegrationTest.kt ================================================ package com.example.redis import org.junit.jupiter.api.extension.ExtendWith import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.util.TestPropertyValues import org.springframework.context.ApplicationContextInitializer import org.springframework.context.ConfigurableApplicationContext import org.springframework.test.context.ContextConfiguration import org.springframework.test.context.junit.jupiter.SpringExtension import org.testcontainers.containers.GenericContainer @SpringBootTest @ContextConfiguration(initializers = [AbstractIntegrationTest.Initializer::class]) @AutoConfigureMockMvc abstract class AbstractIntegrationTest { companion object { val redisContainer = GenericContainer("redis:6-alpine") .apply { withExposedPorts(6379) } } internal class Initializer : ApplicationContextInitializer { override fun initialize(configurableApplicationContext: ConfigurableApplicationContext) { redisContainer.start() TestPropertyValues.of( "spring.redis.host=${redisContainer.host}", "spring.redis.port=${redisContainer.firstMappedPort}" ).applyTo(configurableApplicationContext.environment) } } } ================================================ FILE: examples/spring-boot-kotlin-redis/src/test/kotlin/com/example/redis/RedisApplicationTests.kt ================================================ package com.example.redis import org.junit.jupiter.api.Test class RedisApplicationTests : AbstractIntegrationTest() { @Test fun contextLoads() { } } ================================================ FILE: examples/spring-boot-kotlin-redis/src/test/kotlin/com/example/redis/RedisTest.kt ================================================ package com.example.redis import org.hamcrest.CoreMatchers.containsString import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status class RedisTest : AbstractIntegrationTest() { @Autowired private lateinit var mockMvc: MockMvc @Test fun testRedisFunctionality() { val greeting = "Hello Testcontainers with Kotlin" mockMvc.perform(post("/set-foo").content(greeting)) .andExpect(status().isOk) mockMvc.perform(get("/get-foo")) .andExpect(status().isOk) .andExpect(content().string(containsString(greeting))) } } ================================================ FILE: examples/spring-boot-kotlin-redis/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: examples/zookeeper/build.gradle ================================================ plugins { id 'java' } repositories { mavenCentral() } dependencies { testImplementation 'org.apache.curator:curator-framework:5.9.0' testImplementation 'org.testcontainers:testcontainers' testImplementation 'org.assertj:assertj-core:3.27.4' testImplementation 'ch.qos.logback:logback-classic:1.3.15' testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.11.0' } test { useJUnitPlatform() } ================================================ FILE: examples/zookeeper/src/test/java/com/example/ZookeeperContainerTest.java ================================================ package com.example; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.CuratorFrameworkFactory; import org.apache.curator.retry.RetryOneTime; import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import java.nio.charset.StandardCharsets; import static org.assertj.core.api.Assertions.assertThat; class ZookeeperContainerTest { private static final int ZOOKEEPER_PORT = 2181; @Test void test() throws Exception { String path = "/messages/zk-tc"; String content = "Running Zookeeper with Testcontainers"; try ( GenericContainer zookeeper = new GenericContainer<>("zookeeper:3.8.0").withExposedPorts(ZOOKEEPER_PORT) ) { zookeeper.start(); String connectionString = zookeeper.getHost() + ":" + zookeeper.getMappedPort(ZOOKEEPER_PORT); CuratorFramework curatorFramework = CuratorFrameworkFactory .builder() .connectString(connectionString) .retryPolicy(new RetryOneTime(100)) .build(); curatorFramework.start(); curatorFramework.create().creatingParentsIfNeeded().forPath(path, content.getBytes()); byte[] bytes = curatorFramework.getData().forPath(path); curatorFramework.close(); assertThat(new String(bytes, StandardCharsets.UTF_8)).isEqualTo(content); } } } ================================================ FILE: examples/zookeeper/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: gradle/ci-support.gradle ================================================ import groovy.json.JsonOutput // Emit a JSON-formatted list of check tasks to be run in CI task testMatrix { project.afterEvaluate { def checkTasks = subprojects.collect { it.tasks.findByName("check") }.findAll { it != null } dependsOn(checkTasks) doLast { def checkTaskPaths = checkTasks .collect { it.path } println(JsonOutput.toJson(checkTaskPaths)) } } } // If we're executing the `testMatrix` task, disable tests and other slow tasks // so that we can get a result quickly. gradle.taskGraph.whenReady { if (it.hasTask(tasks.testMatrix)) { for (subproject in subprojects) { subproject.tasks.withType(Test).all { testExecuter([execute: { spec, processor -> }, stopNow:{}] as org.gradle.api.internal.tasks.testing.TestExecuter) } subproject.tasks.findByName("shadowJar")?.enabled = false subproject.tasks.findByName("javadoc")?.enabled = false subproject.tasks.findByName("delombok")?.enabled = false subproject.tasks.findByName("japicmp")?.enabled = false } } } ================================================ FILE: gradle/japicmp.gradle ================================================ configurations { baseline } dependencies { baseline "org.testcontainers:${project.name}:${project['testcontainers.version']}", { exclude group: "*", module: "*" } } tasks.japicmp { dependsOn(tasks.shadowJar) // Disable if baseline dependencies cannot be resolved - such as when developing a new module that doesn't // have an existing published version. enabled = ! configurations.baseline.copy().resolvedConfiguration.lenientConfiguration.getFiles().empty oldClasspath.from(configurations.baseline) newClasspath.from(shadowJar.outputs.files) ignoreMissingClasses = true accessModifier = "protected" failOnModification = true failOnSourceIncompatibility = true onlyBinaryIncompatibleModified = true htmlOutputFile = file("$buildDir/reports/japi.html") packageExcludes = [ "org.testcontainers.shaded.*", ] } // do not run on Windows by default // TODO investigate zip issue on Windows if (!org.gradle.internal.os.OperatingSystem.current().isWindows()) { project.tasks.check.dependsOn(japicmp) } ================================================ FILE: gradle/publishing.gradle ================================================ apply plugin: 'maven-publish' apply plugin: 'org.jreleaser' task sourceJar(type: Jar) { archiveClassifier.set( 'sources') from sourceSets.main.allJava } task javadocJar(type: Jar, dependsOn: javadoc) { archiveClassifier.set('javadoc') from javadoc } jar.archiveClassifier.set("original") publishing { publications { mavenJava(MavenPublication) { publication -> artifactId = project.name artifact sourceJar artifact javadocJar artifact project.tasks.jar artifacts.removeAll { it.classifier == jar.archiveClassifier.get() } artifact project.tasks.shadowJar pom.withXml { def rootNode = asNode() rootNode.children().last() + { resolveStrategy = Closure.DELEGATE_FIRST name project.description description 'Isolated container management for Java code testing' url 'https://java.testcontainers.org' issueManagement { system 'GitHub' url 'https://github.com/testcontainers/testcontainers-java/issues' } licenses { license { name 'MIT' url 'http://opensource.org/licenses/MIT' } } scm { url 'https://github.com/testcontainers/testcontainers-java/' connection 'scm:git:git://github.com/testcontainers/testcontainers-java.git' developerConnection 'scm:git:ssh://git@github.com/testcontainers/testcontainers-java.git' } developers { developer { id 'rnorth' name 'Richard North' email 'rich.north@gmail.com' } } } def dependenciesNode = rootNode.appendNode('dependencies') def apiDeps= project.configurations.api.resolvedConfiguration.firstLevelModuleDependencies def providedDeps = project.configurations.provided.resolvedConfiguration.firstLevelModuleDependencies def newApiDeps = apiDeps - providedDeps def addDependencies = { Set resolvedDependencies, scope -> for (dependency in resolvedDependencies) { if (dependency.configuration.startsWith("platform-")) { continue } dependenciesNode.appendNode('dependency').with { if (!dependency.moduleGroup || !dependency.moduleName || !dependency.moduleVersion) { throw new IllegalStateException("Wrong dependency: $dependency") } appendNode('groupId', dependency.moduleGroup) appendNode('artifactId', dependency.moduleName) appendNode('version', dependency.moduleVersion) appendNode('scope', scope) if (dependency instanceof ModuleDependency && !dependency.excludeRules.empty) { def excludesNode = appendNode('exclusions') for (rule in dependency.excludeRules) { excludesNode.appendNode('exclusion').with { appendNode('groupId', rule.group) appendNode('artifactId', rule.module) } } } } } } addDependencies(newApiDeps, 'compile') addDependencies(providedDeps, 'provided') } } } repositories { maven { url = rootProject.layout.buildDirectory.dir('staging-deploy') } } } jreleaser { signing { active = 'ALWAYS' armored = true } deploy { maven { mavenCentral { central { active = 'ALWAYS' url = 'https://central.sonatype.com/api/v1/publisher' stagingRepository(rootProject.layout.buildDirectory.dir("staging-deploy").get().toString()) stage = 'UPLOAD' applyMavenCentralRules = true namespace = 'org.testcontainers' } } } } } ================================================ FILE: gradle/shading.gradle ================================================ import java.util.jar.JarFile apply plugin: 'com.gradleup.shadow' configurations { shaded [apiElements, implementation]*.extendsFrom shaded } configurations.api.canBeResolved = true shadowJar { configurations = [project.configurations.shaded] archiveClassifier.set(null) mergeServiceFiles() } project.afterEvaluate { Set apiDependencies = project.configurations.api.resolvedConfiguration.resolvedArtifacts*.moduleVersion*.id*.module // Exclude `api` dependencies, to prevent them being included into the final JAR shadowJar.dependencies { for (id in apiDependencies) { exclude(dependency("${id.group}:${id.name}")) } } // Inherit dependencies' relocators shadowJar.relocators = configurations.api.dependencies.withType(ProjectDependency).collectMany { return it.dependencyProject.tasks.findByName("shadowJar")?.relocators ?: [] } // 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) { if (apiDependencies.contains(artifact.moduleVersion.id.module)) { continue } try (def jf = new JarFile(artifact.file)) { for (entry in jf.entries()) { def name = entry.name if (name.endsWith(".class")) { def index = name.lastIndexOf('/') if (index != -1) { packages.add(name.substring(0, index)) } } } } } for (pkg in packages) { pkg = pkg.replaceAll('/', '.') tasks.shadowJar.relocate(pkg, "org.testcontainers.shaded.${pkg}") } // Add shaded JARs first tasks.test.classpath = findShadedProjects(project) .plus(project) .sum { it.tasks.findByName("shadowJar").outputs.files } .plus(sourceSets.test.runtimeClasspath) } static Set findShadedProjects(Project project) { Set dependencies = project.configurations.api.dependencies.withType(ProjectDependency)*.dependencyProject return dependencies + dependencies.collectMany { it -> findShadedProjects(it) } } ================================================ FILE: gradle/spotless.gradle ================================================ apply plugin: 'com.diffplug.spotless' spotless { java { toggleOffOn() removeUnusedImports() trimTrailingWhitespace() endWithNewline() prettier(['prettier': '2.5.1', 'prettier-plugin-java': '1.6.1']) .config([ 'parser' : 'java', 'tabWidth' : 4, 'printWidth': 120 ]) importOrder('', 'java', 'javax', '\\#') } groovyGradle { target '**/*.groovy' greclipse('4.19') } } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionSha256Sum=ed1a8d686605fd7c23bdf62c7fc7add1c5b23b2bbc3721e661934ef4a4911d7c distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ org.gradle.parallel=false org.gradle.caching=true org.gradle.configureondemand=true org.gradle.jvmargs=-Xmx2g testcontainers.version=2.0.4 ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @rem SPDX-License-Identifier: Apache-2.0 @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line set CLASSPATH= @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: mkdocs.yml ================================================ site_name: Testcontainers for Java site_url: https://java.testcontainers.org plugins: - search - codeinclude - markdownextradata theme: name: 'material' custom_dir: 'docs/theme' palette: scheme: testcontainers font: text: Roboto code: Roboto Mono logo: 'logo.svg' favicon: 'favicon.ico' extra_css: - 'css/extra.css' - 'css/tc-header.css' repo_name: 'testcontainers-java' repo_url: 'https://github.com/testcontainers/testcontainers-java' markdown_extensions: - admonition - codehilite: linenums: False - pymdownx.superfences - pymdownx.tabbed: alternate_style: true - pymdownx.snippets nav: - Home: index.md - Quickstart: - quickstart/junit_4_quickstart.md - quickstart/junit_5_quickstart.md - quickstart/spock_quickstart.md - Features: - features/creating_container.md - features/networking.md - features/commands.md - features/files.md - features/startup_and_waits.md - features/container_logs.md - features/creating_images.md - features/jib.md - features/configuration.md - features/image_name_substitution.md - features/advanced_options.md - features/reuse.md - Modules: - Databases: - modules/databases/index.md - modules/databases/jdbc.md - modules/databases/r2dbc.md - modules/databases/cassandra.md - modules/databases/cockroachdb.md - modules/databases/couchbase.md - modules/databases/clickhouse.md - modules/databases/cratedb.md - modules/databases/databend.md - modules/databases/db2.md - modules/databases/influxdb.md - modules/databases/mariadb.md - modules/databases/mongodb.md - modules/databases/mssqlserver.md - modules/databases/mysql.md - modules/databases/neo4j.md - modules/databases/oceanbase.md - modules/databases/oraclefree.md - modules/databases/oraclexe.md - modules/databases/orientdb.md - modules/databases/postgres.md - modules/databases/presto.md - modules/databases/questdb.md - modules/databases/scylladb.md - modules/databases/tidb.md - modules/databases/timeplus.md - modules/databases/trino.md - modules/databases/yugabytedb.md - modules/activemq.md - modules/azure.md - modules/chromadb.md - modules/consul.md - modules/docker_compose.md - modules/docker_mcp_gateway.md - modules/docker_model_runner.md - modules/elasticsearch.md - modules/gcloud.md - modules/grafana.md - modules/hivemq.md - modules/k3s.md - modules/k6.md - modules/kafka.md - modules/ldap.md - modules/localstack.md - modules/milvus.md - modules/minio.md - modules/mockserver.md - modules/nginx.md - modules/ollama.md - modules/openfga.md - modules/pinecone.md - modules/pulsar.md - modules/qdrant.md - modules/rabbitmq.md - modules/redpanda.md - modules/solace.md - modules/solr.md - modules/toxiproxy.md - modules/typesense.md - modules/vault.md - modules/weaviate.md - modules/webdriver_containers.md - Test framework integration: - test_framework_integration/junit_4.md - test_framework_integration/junit_5.md - test_framework_integration/spock.md - test_framework_integration/manual_lifecycle_control.md - test_framework_integration/external.md - Examples: examples.md - System Requirements: - supported_docker_environment/index.md - error_missing_container_runtime_environment.md - Continuous Integration: - supported_docker_environment/continuous_integration/aws_codebuild.md - supported_docker_environment/continuous_integration/dind_patterns.md - supported_docker_environment/continuous_integration/circle_ci.md - supported_docker_environment/continuous_integration/concourse_ci.md - supported_docker_environment/continuous_integration/drone.md - supported_docker_environment/continuous_integration/gitlab_ci.md - supported_docker_environment/continuous_integration/bitbucket_pipelines.md - supported_docker_environment/continuous_integration/tekton.md - supported_docker_environment/continuous_integration/travis.md - supported_docker_environment/windows.md - supported_docker_environment/logging_config.md - supported_docker_environment/image_registry_rate_limiting.md - Getting help: getting_help.md - Contributing: - contributing.md - contributing_docs.md - bounty.md edit_uri: edit/main/docs/ extra: latest_version: 2.0.4 ================================================ FILE: modules/activemq/build.gradle ================================================ description = "Testcontainers :: ActiveMQ" dependencies { api project(':testcontainers') testImplementation "org.apache.activemq:activemq-client:6.2.0" testImplementation "org.apache.activemq:artemis-jakarta-client:2.44.0" } ================================================ FILE: modules/activemq/src/main/java/org/testcontainers/activemq/ActiveMQContainer.java ================================================ package org.testcontainers.activemq; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import java.time.Duration; /** * Testcontainers implementation for Apache ActiveMQ. *

* Exposed ports: *

    *
  • Console: 8161
  • *
  • TCP: 61616
  • *
  • AMQP: 5672
  • *
  • STOMP: 61613
  • *
  • MQTT: 1883
  • *
  • WS: 61614
  • *
*/ public class ActiveMQContainer extends GenericContainer { private static final DockerImageName APACHE_ACTIVEMQ_CLASSIC_IMAGE = DockerImageName.parse( "apache/activemq-classic" ); private static final DockerImageName DEFAULT_IMAGE = DockerImageName.parse("apache/activemq"); private static final int WEB_CONSOLE_PORT = 8161; private static final int TCP_PORT = 61616; private static final int AMQP_PORT = 5672; private static final int STOMP_PORT = 61613; private static final int MQTT_PORT = 1883; private static final int WS_PORT = 61614; private String username; private String password; public ActiveMQContainer(String image) { this(DockerImageName.parse(image)); } public ActiveMQContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE, APACHE_ACTIVEMQ_CLASSIC_IMAGE); withExposedPorts(WEB_CONSOLE_PORT, TCP_PORT, AMQP_PORT, STOMP_PORT, MQTT_PORT, WS_PORT); waitingFor(Wait.forLogMessage(".*Apache ActiveMQ.*started.*", 1).withStartupTimeout(Duration.ofMinutes(1))); } @Override protected void configure() { if (this.username != null) { addEnv("ACTIVEMQ_CONNECTION_USER", this.username); } if (this.password != null) { addEnv("ACTIVEMQ_CONNECTION_PASSWORD", this.password); } } public ActiveMQContainer withUser(String username) { this.username = username; return this; } public ActiveMQContainer withPassword(String password) { this.password = password; return this; } public String getBrokerUrl() { return String.format("tcp://%s:%s", getHost(), getMappedPort(TCP_PORT)); } public String getUser() { return this.username; } public String getPassword() { return this.password; } } ================================================ FILE: modules/activemq/src/main/java/org/testcontainers/activemq/ArtemisContainer.java ================================================ package org.testcontainers.activemq; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import java.time.Duration; /** * Testcontainers implementation for Apache ActiveMQ Artemis. *

* Exposed ports: *

    *
  • Console: 8161
  • *
  • TCP: 61616
  • *
  • HORNETQ: 5445
  • *
  • AMQP: 5672
  • *
  • STOMP: 61613
  • *
  • MQTT: 1883
  • *
  • WS: 61614
  • *
*/ public class ArtemisContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE = DockerImageName.parse("apache/activemq-artemis"); private static final int WEB_CONSOLE_PORT = 8161; // CORE,MQTT,AMQP,HORNETQ,STOMP,OPENWIRE private static final int TCP_PORT = 61616; private static final int HORNETQ_STOMP_PORT = 5445; private static final int AMQP_PORT = 5672; private static final int STOMP_PORT = 61613; private static final int MQTT_PORT = 1883; private static final int WS_PORT = 61614; private String username = "artemis"; private String password = "artemis"; public ArtemisContainer(String image) { this(DockerImageName.parse(image)); } public ArtemisContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE); withExposedPorts(WEB_CONSOLE_PORT, TCP_PORT, HORNETQ_STOMP_PORT, AMQP_PORT, STOMP_PORT, MQTT_PORT, WS_PORT); waitingFor(Wait.forLogMessage(".*HTTP Server started.*", 1).withStartupTimeout(Duration.ofMinutes(1))); } @Override protected void configure() { withEnv("ARTEMIS_USER", this.username); withEnv("ARTEMIS_PASSWORD", this.password); } public ArtemisContainer withUser(String username) { this.username = username; return this; } public ArtemisContainer withPassword(String password) { this.password = password; return this; } public String getBrokerUrl() { return String.format("tcp://%s:%s", getHost(), getMappedPort(TCP_PORT)); } public String getUser() { return getEnvMap().get("ARTEMIS_USER"); } public String getPassword() { return getEnvMap().get("ARTEMIS_PASSWORD"); } } ================================================ FILE: modules/activemq/src/test/java/org/testcontainers/activemq/ActiveMQContainerTest.java ================================================ package org.testcontainers.activemq; import jakarta.jms.Connection; import jakarta.jms.ConnectionFactory; import jakarta.jms.Destination; import jakarta.jms.MessageConsumer; import jakarta.jms.MessageProducer; import jakarta.jms.Session; import jakarta.jms.TextMessage; import lombok.SneakyThrows; import org.apache.activemq.ActiveMQConnectionFactory; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import static org.assertj.core.api.Assertions.assertThat; class ActiveMQContainerTest { @Test void test() { try ( // container { ActiveMQContainer activemq = new ActiveMQContainer("apache/activemq:5.18.7") // } ) { activemq.start(); assertThat(activemq.getUser()).isNull(); assertThat(activemq.getPassword()).isNull(); assertFunctionality(activemq, false); } } @ParameterizedTest @ValueSource(strings = { "apache/activemq-classic:5.18.7", "apache/activemq:5.18.7" }) void compatibility(String image) { try (ActiveMQContainer activemq = new ActiveMQContainer(image)) { activemq.start(); assertFunctionality(activemq, false); } } @Test void customCredentials() { try ( // settingCredentials { ActiveMQContainer activemq = new ActiveMQContainer("apache/activemq:5.18.7") .withUser("testcontainers") .withPassword("testcontainers") // } ) { activemq.start(); assertThat(activemq.getUser()).isEqualTo("testcontainers"); assertThat(activemq.getPassword()).isEqualTo("testcontainers"); assertFunctionality(activemq, true); } } @SneakyThrows private void assertFunctionality(ActiveMQContainer activemq, boolean useCredentials) { ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(activemq.getBrokerUrl()); Connection connection; if (useCredentials) { connection = connectionFactory.createConnection(activemq.getUser(), activemq.getPassword()); } else { connection = connectionFactory.createConnection(); } connection.start(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); Destination destination = session.createQueue("test-queue"); MessageProducer producer = session.createProducer(destination); String contentMessage = "Testcontainers"; TextMessage message = session.createTextMessage(contentMessage); producer.send(message); MessageConsumer consumer = session.createConsumer(destination); TextMessage messageReceived = (TextMessage) consumer.receive(); assertThat(messageReceived.getText()).isEqualTo(contentMessage); } } ================================================ FILE: modules/activemq/src/test/java/org/testcontainers/activemq/ArtemisContainerTest.java ================================================ package org.testcontainers.activemq; import jakarta.jms.Connection; import jakarta.jms.Destination; import jakarta.jms.MessageConsumer; import jakarta.jms.MessageProducer; import jakarta.jms.Session; import jakarta.jms.TextMessage; import lombok.SneakyThrows; import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class ArtemisContainerTest { @Test void defaultCredentials() { try ( // container { ArtemisContainer artemis = new ArtemisContainer("apache/activemq-artemis:2.30.0-alpine") // } ) { artemis.start(); assertThat(artemis.getUser()).isEqualTo("artemis"); assertThat(artemis.getPassword()).isEqualTo("artemis"); assertFunctionality(artemis, false); } } @Test void customCredentials() { try ( // settingCredentials { ArtemisContainer artemis = new ArtemisContainer("apache/activemq-artemis:2.30.0-alpine") .withUser("testcontainers") .withPassword("testcontainers") // } ) { artemis.start(); assertThat(artemis.getUser()).isEqualTo("testcontainers"); assertThat(artemis.getPassword()).isEqualTo("testcontainers"); assertFunctionality(artemis, false); } } @Test void allowAnonymousLogin() { try ( // enableAnonymousLogin { ArtemisContainer artemis = new ArtemisContainer("apache/activemq-artemis:2.30.0-alpine") .withEnv("ANONYMOUS_LOGIN", "true") // } ) { artemis.start(); assertFunctionality(artemis, true); } } @SneakyThrows private void assertFunctionality(ArtemisContainer artemis, boolean anonymousLogin) { ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(artemis.getBrokerUrl()); if (!anonymousLogin) { connectionFactory.setUser(artemis.getUser()); connectionFactory.setPassword(artemis.getPassword()); } Connection connection = connectionFactory.createConnection(); connection.start(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); Destination destination = session.createQueue("test-queue"); MessageProducer producer = session.createProducer(destination); String contentMessage = "Testcontainers"; TextMessage message = session.createTextMessage(contentMessage); producer.send(message); MessageConsumer consumer = session.createConsumer(destination); TextMessage messageReceived = (TextMessage) consumer.receive(); assertThat(messageReceived.getText()).isEqualTo(contentMessage); } } ================================================ FILE: modules/activemq/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/azure/build.gradle ================================================ description = "Testcontainers :: Azure" dependencies { api project(':testcontainers') api project(':testcontainers-mssqlserver') // TODO use JDK's HTTP client and/or Apache HttpClient5 shaded 'com.squareup.okhttp3:okhttp:5.3.2' testImplementation platform("com.azure:azure-sdk-bom:1.2.32") testImplementation 'com.azure:azure-cosmos' testImplementation 'com.azure:azure-storage-blob' testImplementation 'com.azure:azure-storage-queue' testImplementation 'com.azure:azure-data-tables' testImplementation 'com.azure:azure-messaging-eventhubs' testImplementation 'com.azure:azure-messaging-servicebus' testImplementation 'com.microsoft.sqlserver:mssql-jdbc:13.3.0.jre8-preview' } tasks.japicmp { methodExcludes = ["org.testcontainers.azure.ServiceBusEmulatorContainer#withMsSqlServerContainer(org.testcontainers.containers.MSSQLServerContainer)"] } ================================================ FILE: modules/azure/src/main/java/org/testcontainers/azure/AzuriteContainer.java ================================================ package org.testcontainers.azure; import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; /** * Testcontainers implementation for Azurite Emulator. *

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

* Exposed ports: *

    *
  • Blob: 10000
  • *
  • Queue: 10001
  • *
  • Table: 10002
  • *
*/ public class AzuriteContainer extends GenericContainer { private static final String ALLOW_ALL_CONNECTIONS = "0.0.0.0"; private static final int DEFAULT_BLOB_PORT = 10000; private static final int DEFAULT_QUEUE_PORT = 10001; private static final int DEFAULT_TABLE_PORT = 10002; private static final String CONNECTION_STRING_FORMAT = "DefaultEndpointsProtocol=%s;AccountName=%s;AccountKey=%s;BlobEndpoint=%s://%s:%d/%s;QueueEndpoint=%s://%s:%d/%s;TableEndpoint=%s://%s:%d/%s;"; /** * The account name of the default credentials. */ private static final String WELL_KNOWN_ACCOUNT_NAME = "devstoreaccount1"; /** * The account key of the default credentials. */ private static final String WELL_KNOWN_ACCOUNT_KEY = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse( "mcr.microsoft.com/azure-storage/azurite" ); private MountableFile cert = null; private String certExtension = null; private MountableFile key = null; private String pwd = null; /** * @param dockerImageName specified docker image name to run */ public AzuriteContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } /** * @param dockerImageName specified docker image name to run */ public AzuriteContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); withExposedPorts(DEFAULT_BLOB_PORT, DEFAULT_QUEUE_PORT, DEFAULT_TABLE_PORT); } /** * Configure SSL with a custom certificate and password. * * @param pfxCert The PFX certificate file * @param password The password securing the certificate * @return this */ public AzuriteContainer withSsl(final MountableFile pfxCert, final String password) { this.cert = pfxCert; this.pwd = password; this.certExtension = ".pfx"; return this; } /** * Configure SSL with a custom certificate and private key. * * @param pemCert The PEM certificate file * @param pemKey The PEM key file * @return this */ public AzuriteContainer withSsl(final MountableFile pemCert, final MountableFile pemKey) { this.cert = pemCert; this.key = pemKey; this.certExtension = ".pem"; return this; } @Override protected void configure() { withCommand(getCommandLine()); if (this.cert != null) { logger().info("Using path for cert file: '{}'", this.cert); withCopyFileToContainer(this.cert, "/cert" + this.certExtension); if (this.key != null) { logger().info("Using path for key file: '{}'", this.key); withCopyFileToContainer(this.key, "/key.pem"); } } } /** * Returns the connection string for the default credentials. * * @return connection string */ public String getConnectionString() { return getConnectionString(WELL_KNOWN_ACCOUNT_NAME, WELL_KNOWN_ACCOUNT_KEY); } /** * Returns the connection string for the account name and key specified. * * @param accountName The name of the account * @param accountKey The account key * @return connection string */ public String getConnectionString(final String accountName, final String accountKey) { final String protocol = cert != null ? "https" : "http"; return String.format( CONNECTION_STRING_FORMAT, protocol, accountName, accountKey, protocol, getHost(), getMappedPort(DEFAULT_BLOB_PORT), accountName, protocol, getHost(), getMappedPort(DEFAULT_QUEUE_PORT), accountName, protocol, getHost(), getMappedPort(DEFAULT_TABLE_PORT), accountName ); } String getCommandLine() { final StringBuilder args = new StringBuilder("azurite"); args.append(" --blobHost ").append(ALLOW_ALL_CONNECTIONS); args.append(" --queueHost ").append(ALLOW_ALL_CONNECTIONS); args.append(" --tableHost ").append(ALLOW_ALL_CONNECTIONS); if (this.cert != null) { args.append(" --cert ").append("/cert").append(this.certExtension); if (this.pwd != null) { args.append(" --pwd ").append(this.pwd); } else { args.append(" --key ").append("/key.pem"); } } final String cmd = args.toString(); logger().debug("Using command line: '{}'", cmd); return cmd; } } ================================================ FILE: modules/azure/src/main/java/org/testcontainers/azure/EventHubsEmulatorContainer.java ================================================ package org.testcontainers.azure; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.LicenseAcceptance; /** * Testcontainers implementation for Azure Eventhubs Emulator. *

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

* Exposed ports: *

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

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

* Exposed port: 5672 */ public class ServiceBusEmulatorContainer extends GenericContainer { private static final String CONNECTION_STRING_FORMAT = "Endpoint=sb://%s:%d;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"; private static final int DEFAULT_PORT = 5672; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse( "mcr.microsoft.com/azure-messaging/servicebus-emulator" ); private MSSQLServerContainer msSqlServerContainer; /** * @param dockerImageName The specified docker image name to run */ public ServiceBusEmulatorContainer(final String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } /** * @param dockerImageName The specified docker image name to run */ public ServiceBusEmulatorContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); withExposedPorts(DEFAULT_PORT); withEnv("SQL_WAIT_INTERVAL", "0"); waitingFor(Wait.forLogMessage(".*Emulator Service is Successfully Up!.*", 1)); } /** * Sets the MS SQL Server dependency needed by the Service Bus Container, * * @param msSqlServerContainer The MS SQL Server container used by Service Bus as a dependency * @return this */ public ServiceBusEmulatorContainer withMsSqlServerContainer(final MSSQLServerContainer msSqlServerContainer) { dependsOn(msSqlServerContainer); this.msSqlServerContainer = msSqlServerContainer; return this; } /** * Provide the Service Bus configuration JSON. * * @param config The configuration * @return this */ public ServiceBusEmulatorContainer withConfig(final Transferable config) { withCopyToContainer(config, "/ServiceBus_Emulator/ConfigFiles/Config.json"); return this; } /** * Accepts the EULA of the container. * * @return this */ public ServiceBusEmulatorContainer acceptLicense() { withEnv("ACCEPT_EULA", "Y"); return this; } @Override protected void configure() { if (msSqlServerContainer == null) { throw new IllegalStateException( "The image " + getDockerImageName() + " requires a Microsoft SQL Server container. Please provide one with the withMsSqlServerContainer method!" ); } withEnv("SQL_SERVER", msSqlServerContainer.getNetworkAliases().get(0)); withEnv("MSSQL_SA_PASSWORD", msSqlServerContainer.getPassword()); // If license was not accepted programmatically, check if it was accepted via resource file if (!getEnvMap().containsKey("ACCEPT_EULA")) { LicenseAcceptance.assertLicenseAccepted(this.getDockerImageName()); acceptLicense(); } } /** * Returns the connection string. * * @return connection string */ public String getConnectionString() { return String.format(CONNECTION_STRING_FORMAT, getHost(), getMappedPort(DEFAULT_PORT)); } } ================================================ FILE: modules/azure/src/main/java/org/testcontainers/containers/CosmosDBEmulatorContainer.java ================================================ package org.testcontainers.containers; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import java.security.KeyStore; /** * Testcontainers implementation for CosmosDB Emulator. *

* Supported image: {@code mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator} *

* Exposed ports: 8081 */ public class CosmosDBEmulatorContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse( "mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator" ); private static final int PORT = 8081; /** * @param dockerImageName specified docker image name to run */ public CosmosDBEmulatorContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); withExposedPorts(PORT); waitingFor(Wait.forLogMessage(".*Started\\r\\n$", 1)); } /** * @return new KeyStore built with PKCS12 */ public KeyStore buildNewKeyStore() { return KeyStoreBuilder.buildByDownloadingCertificate(getEmulatorEndpoint(), getEmulatorKey()); } /** * Emulator key is a known constant and specified in Azure Cosmos DB Documents. * This key is also used as password for emulator certificate file. * * @return predefined emulator key * @see Azure Cosmos DB Documents */ public String getEmulatorKey() { return "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; } /** * @return secure https emulator endpoint to send requests */ public String getEmulatorEndpoint() { return "https://" + getHost() + ":" + getMappedPort(PORT); } } ================================================ FILE: modules/azure/src/main/java/org/testcontainers/containers/KeyStoreBuilder.java ================================================ package org.testcontainers.containers; import okhttp3.Cache; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import java.io.InputStream; import java.security.KeyStore; import java.security.SecureRandom; import java.security.cert.Certificate; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.Objects; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; final class KeyStoreBuilder { static KeyStore buildByDownloadingCertificate(String endpoint, String keyStorePassword) { OkHttpClient client = null; Response response = null; try { TrustManager[] trustAllManagers = buildTrustAllManagers(); client = buildTrustAllClient(trustAllManagers); Request request = buildRequest(endpoint); response = client.newCall(request).execute(); return buildKeyStore(response.body().byteStream(), keyStorePassword); } catch (Exception ex) { throw new IllegalStateException(ex); } finally { closeResponseSilently(response); closeClientSilently(client); } } private static TrustManager[] buildTrustAllManagers() { return new TrustManager[] { new X509TrustManager() { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) {} @Override public void checkServerTrusted(X509Certificate[] chain, String authType) {} @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[] {}; } }, }; } private static OkHttpClient buildTrustAllClient(TrustManager[] trustManagers) throws Exception { SSLContext sslContext = SSLContext.getInstance("SSL"); sslContext.init(null, trustManagers, new SecureRandom()); SSLSocketFactory socketFactory = sslContext.getSocketFactory(); return new OkHttpClient.Builder() .sslSocketFactory(socketFactory, (X509TrustManager) trustManagers[0]) .hostnameVerifier((s, sslSession) -> true) .build(); } private static Request buildRequest(String endpoint) { return new Request.Builder().get().url(endpoint + "/_explorer/emulator.pem").build(); } private static KeyStore buildKeyStore(InputStream certificateStream, String keyStorePassword) throws Exception { Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(certificateStream); KeyStore keystore = KeyStore.getInstance("PKCS12"); keystore.load(null, keyStorePassword.toCharArray()); keystore.setCertificateEntry("azure-cosmos-emulator", certificate); return keystore; } private static void closeResponseSilently(Response response) { try { if (Objects.nonNull(response)) { response.close(); } } catch (Exception ignored) {} } private static void closeClientSilently(OkHttpClient client) { try { if (Objects.nonNull(client)) { client.dispatcher().executorService().shutdown(); client.connectionPool().evictAll(); Cache cache = client.cache(); if (Objects.nonNull(cache)) { cache.close(); } } } catch (Exception ignored) {} } } ================================================ FILE: modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerTest.java ================================================ package org.testcontainers.azure; import com.azure.core.util.BinaryData; import com.azure.data.tables.TableClient; import com.azure.data.tables.TableServiceClient; import com.azure.data.tables.TableServiceClientBuilder; import com.azure.storage.blob.BlobClient; import com.azure.storage.blob.BlobContainerClient; import com.azure.storage.blob.BlobServiceClient; import com.azure.storage.blob.BlobServiceClientBuilder; import com.azure.storage.queue.QueueClient; import com.azure.storage.queue.QueueServiceClient; import com.azure.storage.queue.QueueServiceClientBuilder; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.testcontainers.utility.MountableFile; import java.util.Properties; import static org.assertj.core.api.Assertions.assertThat; class AzuriteContainerTest { private static final String PASSWORD = "changeit"; private static Properties originalSystemProperties; @BeforeAll public static void captureOriginalSystemProperties() { originalSystemProperties = (Properties) System.getProperties().clone(); System.setProperty( "javax.net.ssl.trustStore", MountableFile.forClasspathResource("/keystore.pfx").getFilesystemPath() ); System.setProperty("javax.net.ssl.trustStorePassword", PASSWORD); System.setProperty("javax.net.ssl.trustStoreType", "PKCS12"); } @AfterAll public static void restoreOriginalSystemProperties() { System.setProperties(originalSystemProperties); } @Test void testWithBlobServiceClient() { try ( // emulatorContainer { AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0") // } ) { emulator.start(); assertThat(emulator.getConnectionString()).contains("BlobEndpoint=http://"); testBlob(emulator); } } @Test void testWithQueueServiceClient() { try (AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0")) { emulator.start(); assertThat(emulator.getConnectionString()).contains("QueueEndpoint=http://"); testQueue(emulator); } } @Test void testWithTableServiceClient() { try (AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0")) { emulator.start(); assertThat(emulator.getConnectionString()).contains("TableEndpoint=http://"); testTable(emulator); } } @Test void testWithBlobServiceClientWithSslUsingPfx() { try ( AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0") .withSsl(MountableFile.forClasspathResource("/keystore.pfx"), PASSWORD) ) { emulator.start(); assertThat(emulator.getConnectionString()).contains("BlobEndpoint=https://"); testBlob(emulator); } } @Test void testWithQueueServiceClientWithSslUsingPfx() { try ( AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0") .withSsl(MountableFile.forClasspathResource("/keystore.pfx"), PASSWORD) ) { emulator.start(); assertThat(emulator.getConnectionString()).contains("QueueEndpoint=https://"); testQueue(emulator); } } @Test void testWithTableServiceClientWithSslUsingPfx() { try ( AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0") .withSsl(MountableFile.forClasspathResource("/keystore.pfx"), PASSWORD) ) { emulator.start(); assertThat(emulator.getConnectionString()).contains("TableEndpoint=https://"); testTable(emulator); } } @Test void testWithBlobServiceClientWithSslUsingPem() { try ( AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0") .withSsl( MountableFile.forClasspathResource("/certificate.pem"), MountableFile.forClasspathResource("/key.pem") ) ) { emulator.start(); assertThat(emulator.getConnectionString()).contains("BlobEndpoint=https://"); testBlob(emulator); } } @Test void testWithQueueServiceClientWithSslUsingPem() { try ( AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0") .withSsl( MountableFile.forClasspathResource("/certificate.pem"), MountableFile.forClasspathResource("/key.pem") ) ) { emulator.start(); assertThat(emulator.getConnectionString()).contains("QueueEndpoint=https://"); testQueue(emulator); } } @Test void testWithTableServiceClientWithSslUsingPem() { try ( AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0") .withSsl( MountableFile.forClasspathResource("/certificate.pem"), MountableFile.forClasspathResource("/key.pem") ) ) { emulator.start(); assertThat(emulator.getConnectionString()).contains("TableEndpoint=https://"); testTable(emulator); } } @Test void testTwoAccountKeysWithBlobServiceClient() { try ( // withTwoAccountKeys { AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0") .withEnv("AZURITE_ACCOUNTS", "account1:key1:key2") // } ) { emulator.start(); String connectionString1 = emulator.getConnectionString("account1", "key1"); // the second account will have access to the same container using a different key String connectionString2 = emulator.getConnectionString("account1", "key2"); BlobServiceClient blobServiceClient1 = new BlobServiceClientBuilder() .connectionString(connectionString1) .buildClient(); BlobContainerClient containerClient1 = blobServiceClient1.createBlobContainer("test-container"); BlobClient blobClient1 = containerClient1.getBlobClient("test-blob.txt"); blobClient1.upload(BinaryData.fromString("content")); boolean existsWithAccount1 = blobClient1.exists(); String contentWithAccount1 = blobClient1.downloadContent().toString(); BlobServiceClient blobServiceClient2 = new BlobServiceClientBuilder() .connectionString(connectionString2) .buildClient(); BlobContainerClient containerClient2 = blobServiceClient2.getBlobContainerClient("test-container"); BlobClient blobClient2 = containerClient2.getBlobClient("test-blob.txt"); boolean existsWithAccount2 = blobClient2.exists(); String contentWithAccount2 = blobClient2.downloadContent().toString(); assertThat(existsWithAccount1).isTrue(); assertThat(contentWithAccount1).isEqualTo("content"); assertThat(existsWithAccount2).isTrue(); assertThat(contentWithAccount2).isEqualTo("content"); } } @Test void testMultipleAccountsWithBlobServiceClient() { try ( // withMoreAccounts { AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0") .withEnv("AZURITE_ACCOUNTS", "account1:key1;account2:key2") // } ) { emulator.start(); // useNonDefaultCredentials { String connectionString1 = emulator.getConnectionString("account1", "key1"); // the second account will not have access to the same container String connectionString2 = emulator.getConnectionString("account2", "key2"); // } BlobServiceClient blobServiceClient1 = new BlobServiceClientBuilder() .connectionString(connectionString1) .buildClient(); BlobContainerClient containerClient1 = blobServiceClient1.createBlobContainer("test-container"); BlobClient blobClient1 = containerClient1.getBlobClient("test-blob.txt"); blobClient1.upload(BinaryData.fromString("content")); boolean existsWithAccount1 = blobClient1.exists(); String contentWithAccount1 = blobClient1.downloadContent().toString(); BlobServiceClient blobServiceClient2 = new BlobServiceClientBuilder() .connectionString(connectionString2) .buildClient(); BlobContainerClient containerClient2 = blobServiceClient2.createBlobContainer("test-container"); BlobClient blobClient2 = containerClient2.getBlobClient("test-blob.txt"); boolean existsWithAccount2 = blobClient2.exists(); assertThat(existsWithAccount1).isTrue(); assertThat(contentWithAccount1).isEqualTo("content"); assertThat(existsWithAccount2).isFalse(); } } private void testBlob(AzuriteContainer container) { // createBlobClient { BlobServiceClient blobServiceClient = new BlobServiceClientBuilder() .connectionString(container.getConnectionString()) .buildClient(); // } BlobContainerClient containerClient = blobServiceClient.createBlobContainer("test-container"); assertThat(containerClient.exists()).isTrue(); } private void testQueue(AzuriteContainer container) { // createQueueClient { QueueServiceClient queueServiceClient = new QueueServiceClientBuilder() .connectionString(container.getConnectionString()) .buildClient(); // } QueueClient queueClient = queueServiceClient.createQueue("test-queue"); assertThat(queueClient.getQueueUrl()).isNotNull(); } private void testTable(AzuriteContainer container) { // createTableClient { TableServiceClient tableServiceClient = new TableServiceClientBuilder() .connectionString(container.getConnectionString()) .buildClient(); // } TableClient tableClient = tableServiceClient.createTable("testtable"); assertThat(tableClient.getTableEndpoint()).isNotNull(); } } ================================================ FILE: modules/azure/src/test/java/org/testcontainers/azure/EventHubsEmulatorContainerTest.java ================================================ package org.testcontainers.azure; import com.azure.core.util.IterableStream; import com.azure.messaging.eventhubs.EventData; import com.azure.messaging.eventhubs.EventHubClientBuilder; import com.azure.messaging.eventhubs.EventHubConsumerClient; import com.azure.messaging.eventhubs.EventHubProducerClient; import com.azure.messaging.eventhubs.models.EventPosition; import com.azure.messaging.eventhubs.models.PartitionEvent; import org.junit.jupiter.api.Test; import org.testcontainers.containers.Network; import org.testcontainers.utility.MountableFile; import java.time.Duration; import java.util.Collections; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.waitAtMost; class EventHubsEmulatorContainerTest { @Test public void testWithEventHubsClient() { try ( // network { Network network = Network.newNetwork(); // } // azuriteContainer { AzuriteContainer azuriteContainer = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0") .withNetwork(network); // } // emulatorContainer { EventHubsEmulatorContainer emulator = new EventHubsEmulatorContainer( "mcr.microsoft.com/azure-messaging/eventhubs-emulator:2.0.1" ) .acceptLicense() .withNetwork(network) .withConfig(MountableFile.forClasspathResource("/eventhubs_config.json")) .withAzuriteContainer(azuriteContainer); // } ) { emulator.start(); // createProducerAndConsumer { EventHubProducerClient producer = new EventHubClientBuilder() .connectionString(emulator.getConnectionString()) .fullyQualifiedNamespace("emulatorNs1") .eventHubName("eh1") .buildProducerClient(); EventHubConsumerClient consumer = new EventHubClientBuilder() .connectionString(emulator.getConnectionString()) .fullyQualifiedNamespace("emulatorNs1") .eventHubName("eh1") .consumerGroup("cg1") .buildConsumerClient(); // } producer.send(Collections.singletonList(new EventData("test"))); waitAtMost(Duration.ofSeconds(30)) .pollDelay(Duration.ofSeconds(5)) .untilAsserted(() -> { IterableStream events = consumer.receiveFromPartition( "0", 1, EventPosition.earliest(), Duration.ofSeconds(2) ); Optional event = events.stream().findFirst(); assertThat(event).isPresent(); assertThat(event.get().getData().getBodyAsString()).isEqualTo("test"); }); } } } ================================================ FILE: modules/azure/src/test/java/org/testcontainers/azure/ServiceBusEmulatorContainerTest.java ================================================ package org.testcontainers.azure; import com.azure.messaging.servicebus.ServiceBusClientBuilder; import com.azure.messaging.servicebus.ServiceBusErrorContext; import com.azure.messaging.servicebus.ServiceBusException; import com.azure.messaging.servicebus.ServiceBusMessage; import com.azure.messaging.servicebus.ServiceBusProcessorClient; import com.azure.messaging.servicebus.ServiceBusReceivedMessageContext; import com.azure.messaging.servicebus.ServiceBusSenderClient; import com.github.dockerjava.api.model.Capability; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.testcontainers.containers.Network; import org.testcontainers.mssqlserver.MSSQLServerContainer; import org.testcontainers.utility.MountableFile; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; class ServiceBusEmulatorContainerTest { @Test void testWithClient() { try ( // network { Network network = Network.newNetwork(); // } // sqlContainer { MSSQLServerContainer mssqlServerContainer = new MSSQLServerContainer( "mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04" ) .acceptLicense() .withPassword("yourStrong(!)Password") .withCreateContainerCmdModifier(cmd -> { cmd.getHostConfig().withCapAdd(Capability.SYS_PTRACE); }) .withNetwork(network); // } // emulatorContainer { ServiceBusEmulatorContainer emulator = new ServiceBusEmulatorContainer( "mcr.microsoft.com/azure-messaging/servicebus-emulator:1.1.2" ) .acceptLicense() .withConfig(MountableFile.forClasspathResource("/service-bus-config.json")) .withNetwork(network) .withMsSqlServerContainer(mssqlServerContainer); // } ) { emulator.start(); assertThat(emulator.getConnectionString()).startsWith("Endpoint=sb://"); // senderClient { ServiceBusSenderClient senderClient = new ServiceBusClientBuilder() .connectionString(emulator.getConnectionString()) .sender() .queueName("queue.1") .buildClient(); // } await() .atMost(20, TimeUnit.SECONDS) .ignoreException(ServiceBusException.class) .until(() -> { senderClient.sendMessage(new ServiceBusMessage("Hello, Testcontainers!")); return true; }); senderClient.close(); final List received = new CopyOnWriteArrayList<>(); Consumer messageConsumer = m -> { received.add(m.getMessage().getBody().toString()); m.complete(); }; Consumer errorConsumer = e -> Assertions.fail("Unexpected error: " + e); // processorClient { ServiceBusProcessorClient processorClient = new ServiceBusClientBuilder() .connectionString(emulator.getConnectionString()) .processor() .queueName("queue.1") .processMessage(messageConsumer) .processError(errorConsumer) .buildProcessorClient(); // } processorClient.start(); await() .atMost(20, TimeUnit.SECONDS) .untilAsserted(() -> { assertThat(received).hasSize(1).containsExactlyInAnyOrder("Hello, Testcontainers!"); }); processorClient.close(); } } } ================================================ FILE: modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java ================================================ package org.testcontainers.containers; import com.azure.cosmos.CosmosAsyncClient; import com.azure.cosmos.CosmosClientBuilder; import com.azure.cosmos.models.CosmosContainerResponse; import com.azure.cosmos.models.CosmosDatabaseResponse; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.testcontainers.utility.DockerImageName; import java.io.FileOutputStream; import java.nio.file.Path; import java.security.KeyStore; import java.util.Properties; import static org.assertj.core.api.Assertions.assertThat; class CosmosDBEmulatorContainerTest { private static Properties originalSystemProperties; @BeforeAll public static void captureOriginalSystemProperties() { originalSystemProperties = (Properties) System.getProperties().clone(); } @AfterAll public static void restoreOriginalSystemProperties() { System.setProperties(originalSystemProperties); } @TempDir public Path tempFolder; @Test void testWithCosmosClient() throws Exception { try ( // emulatorContainer { CosmosDBEmulatorContainer emulator = new CosmosDBEmulatorContainer( DockerImageName.parse("mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:latest") ); // } ) { emulator.start(); // buildAndSaveNewKeyStore { Path keyStoreFile = tempFolder.resolve("azure-cosmos-emulator.keystore"); KeyStore keyStore = emulator.buildNewKeyStore(); keyStore.store(new FileOutputStream(keyStoreFile.toFile()), emulator.getEmulatorKey().toCharArray()); // } // setSystemTrustStoreParameters { System.setProperty("javax.net.ssl.trustStore", keyStoreFile.toString()); System.setProperty("javax.net.ssl.trustStorePassword", emulator.getEmulatorKey()); System.setProperty("javax.net.ssl.trustStoreType", "PKCS12"); // } // buildClient { CosmosAsyncClient client = new CosmosClientBuilder() .gatewayMode() .endpointDiscoveryEnabled(false) .endpoint(emulator.getEmulatorEndpoint()) .key(emulator.getEmulatorKey()) .buildAsyncClient(); // } // testWithClientAgainstEmulatorContainer { CosmosDatabaseResponse databaseResponse = client.createDatabaseIfNotExists("Azure").block(); assertThat(databaseResponse.getStatusCode()).isEqualTo(201); CosmosContainerResponse containerResponse = client .getDatabase("Azure") .createContainerIfNotExists("ServiceContainer", "/name") .block(); assertThat(containerResponse.getStatusCode()).isEqualTo(201); // } } } } ================================================ FILE: modules/azure/src/test/resources/certificate.pem ================================================ Bag Attributes friendlyName: localhost localKeyID: 54 69 6D 65 20 31 37 33 34 37 32 32 33 32 31 33 31 39 subject=CN = localhost issuer=CN = localhost -----BEGIN CERTIFICATE----- MIIC5zCCAc+gAwIBAgIILe7i2bhRE5cwDQYJKoZIhvcNAQEMBQAwFDESMBAGA1UE AxMJbG9jYWxob3N0MB4XDTI0MTIyMDE5MTg0MVoXDTQ0MTIxNTE5MTg0MVowFDES MBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC AQEAoqYNmLl8IiIoYrXdcoWiMQaM0lOHcV9v3A/THMremHxsR+JPm3FIOAuilFcy my16kuXIWHfisPxUWr9Vbf8wP/WwZutoOofJrqmruZoorQcNLCs8mQweguRmL1ju /lDh/9bP626vP9OjwStC4UO4f8Jga8ENoH1U+j1RsIAswYnkk3YIN6YrYv66UvtH IfR0ERgid2LMBIM+2KD2zw4QRyqXH7Qvo7sCsxdYYHGa6GXfza4vgvce9kJwGqn5 wiF0Uw9XQbr/LarnR2GCy020OB81KweQJNIh27FZSRLtT+XpsjDRcC2aLBd8CRHd hwO2zAPI04dLbLM5XAHlEdfT7wIDAQABoz0wOzAdBgNVHQ4EFgQUPqY5isb6Q11Q t6dbXYHEupxADdMwGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3 DQEBDAUAA4IBAQA2katMXrTJBukiNh9yceLO/MewsxvU3KOO/O89ngfjhKXm9T8E RtENCmp7hLbj1Aj4PRZx3AbmUt9+tRu8fmrRXJQWgUDSHJWjDwSTBOaHcC5LDWSU Ex4co5Mnxvrimg7tqQg82Hw/yLH9j6gyTyh6v45QETP7IUkTZe4fg75/kPjng7Xg wp/QXFUx/f0dbvGRl2Fdgg0SnYFqHS3MFIjjFjv8SQlV7rZe+CD1Lxqy/Z6Fd/Fa 33TzTuJeSAG43vdkGAvsNK/KdnxAW03T4l3pVHpNPcvsIvJUMeKOwYOjwHF/eowk tGrKbpUYFxUr9iKHTfu14t1oExhAsnda2Fcs -----END CERTIFICATE----- ================================================ FILE: modules/azure/src/test/resources/eventhubs_config.json ================================================ { "UserConfig": { "NamespaceConfig": [ { "Type": "EventHub", "Name": "emulatorNs1", "Entities": [ { "Name": "eh1", "PartitionCount": "1", "ConsumerGroups": [ { "Name": "cg1" } ] } ] } ], "LoggingConfig": { "Type": "File" } } } ================================================ FILE: modules/azure/src/test/resources/key.pem ================================================ Bag Attributes friendlyName: localhost localKeyID: 54 69 6D 65 20 31 37 33 34 37 32 32 33 32 31 33 31 39 Key Attributes: -----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCipg2YuXwiIihi td1yhaIxBozSU4dxX2/cD9Mcyt6YfGxH4k+bcUg4C6KUVzKbLXqS5chYd+Kw/FRa v1Vt/zA/9bBm62g6h8muqau5miitBw0sKzyZDB6C5GYvWO7+UOH/1s/rbq8/06PB K0LhQ7h/wmBrwQ2gfVT6PVGwgCzBieSTdgg3piti/rpS+0ch9HQRGCJ3YswEgz7Y oPbPDhBHKpcftC+juwKzF1hgcZroZd/Nri+C9x72QnAaqfnCIXRTD1dBuv8tqudH YYLLTbQ4HzUrB5Ak0iHbsVlJEu1P5emyMNFwLZosF3wJEd2HA7bMA8jTh0tsszlc AeUR19PvAgMBAAECggEAFT8dzZKFTawqnGJncBtWyZKyeJMiwUOXSCblDADQPRkb x/QfNA4DQhb7AOe3G6BAP8o2dqAKg9YiasxNq5XHRsOgbIFZ1zN/vAo7/X3OzHN8 XAW138Q+hBiz5IF4js4gB5yXAokt6WeLH6O4E9cV1dKdZ9YLIqjcnee+sRC9R/a3 CexqLfC6b77JFbtePfq+5cn2RiK540tO/4k+F+kfJtTg78Wf2RB3A0pBAunhPSd5 eyjiSvOZtTcvl4GdYw9nKf24I1/WUvt9FH/r1XG0CM5iwuGodbBz1iSUDaQGi5Lf hFWofXt7eebgsPEKciG4xTyk51p9fy9y8asY+jCbkQKBgQDG2UqJToR8G4Fk6uaO /XJa15TibIwQDEota0OdlXg2ZR4864fkIBv+UTbymZEM/EBuSdMM1CUBDvYHcFQX Aj8p1LUyKP2QYwxV/OoPfJ5fBqxqONNR1fLFg7xCxnf9kSvsni2WFneQUrTDl8+7 qnHm4IKPkAxZ4Orxl5qIBmlpGQKBgQDRZUL3cHIVLLg/aZACpo6SYDYg2bztXmz4 lRk9j17q1uS83Umzd2lPFmSt/Nr85EKraxXZ/lYPKrP/r1pf1/35eXOWqmYBWgo/ Hh7OzL12bhvv9UWEY/TvW+wNJNtXlJSjEFRN4tjoG2amYumyhwMO1lIulplUWvtw ymm8hDjeRwKBgCq7n60KVqZlMtWBNbMc/GpRUgmm0iLQwVApcQp4iLEH4gutgjKg Q+PPiENyhR2JSD9rVhO3s4warvzCQw/+x5wxvg7diEBzSL9h7tsNKOu6/2qEc8Vu eRHBUb/37ulrPUlIZPuQMHmvjHFMOrRV2MyJCwXXKxBVqafpsKfy2MxhAoGBAIHH Cswk6u/ouYDDwjeCVxatfp65lHhhb5RZhD09IIzYBwhu9gC+34veyyNydZ8LMa7g PbjQAzJ/OvQbEB4a1hPKjDMzBOmNjpAz8NAm4L4H3FTKZP16nhHDnPdAgpkzQzQV KMrk755bbTFuWH0HZIPLnT+2ou0/PltXeFUYdc59AoGBAIGfWgSOiw7aXbSQZFrO 4S0v3VTwTaiGDVS4pkNRLlhEJUhy8+gbLv/zYDmFmGtqVhXTb/nd6DOdylp+W/HS 8xNWBMWdlX/hVdSK7M0TdJvAaCaMidlquf5qZ2tGNNDeTUN1qbRH26pm8vdNZ3gr Y/WWJGo0iEmwyB8RcFhvNmuJ -----END PRIVATE KEY----- ================================================ FILE: modules/azure/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/azure/src/test/resources/service-bus-config.json ================================================ { "UserConfig": { "Namespaces": [ { "Name": "sbemulatorns", "Queues": [ { "Name": "queue.1", "Properties": { "DeadLetteringOnMessageExpiration": false, "DefaultMessageTimeToLive": "PT1H", "DuplicateDetectionHistoryTimeWindow": "PT20S", "ForwardDeadLetteredMessagesTo": "", "ForwardTo": "", "LockDuration": "PT1M", "MaxDeliveryCount": 3, "RequiresDuplicateDetection": false, "RequiresSession": false } } ], "Topics": [] } ], "Logging": { "Type": "File" } } } ================================================ FILE: modules/build.gradle ================================================ ================================================ FILE: modules/cassandra/build.gradle ================================================ description = "Testcontainers :: Cassandra" configurations.all { resolutionStrategy { force 'io.dropwizard.metrics:metrics-core:3.2.6' } } dependencies { api project(":testcontainers-database-commons") api "com.datastax.cassandra:cassandra-driver-core:3.10.0" testImplementation 'com.datastax.oss:java-driver-core:4.17.0' } ================================================ FILE: modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraContainer.java ================================================ package org.testcontainers.cassandra; import com.github.dockerjava.api.command.InspectContainerResponse; import org.apache.commons.lang3.StringUtils; import org.testcontainers.containers.GenericContainer; import org.testcontainers.ext.ScriptUtils; import org.testcontainers.ext.ScriptUtils.ScriptLoadException; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.net.InetSocketAddress; import java.util.Optional; /** * Testcontainers implementation for Apache Cassandra. *

* Supported image: {@code cassandra} *

* Exposed ports: 9042 */ public class CassandraContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("cassandra"); private static final Integer CQL_PORT = 9042; private static final String DEFAULT_LOCAL_DATACENTER = "datacenter1"; private static final String DEFAULT_INIT_SCRIPT_FILENAME = "init.cql"; private static final String CONTAINER_CONFIG_LOCATION = "/etc/cassandra"; private static final String USERNAME = "cassandra"; private static final String PASSWORD = "cassandra"; private String configLocation; private String initScriptPath; private String clientCertFile; private String clientKeyFile; public CassandraContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public CassandraContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); addExposedPort(CQL_PORT); withEnv("CASSANDRA_SNITCH", "GossipingPropertyFileSnitch"); withEnv("JVM_OPTS", "-Dcassandra.skip_wait_for_gossip_to_settle=0 -Dcassandra.initial_token=0"); withEnv("HEAP_NEWSIZE", "128M"); withEnv("MAX_HEAP_SIZE", "1024M"); withEnv("CASSANDRA_ENDPOINT_SNITCH", "GossipingPropertyFileSnitch"); withEnv("CASSANDRA_DC", DEFAULT_LOCAL_DATACENTER); // Use the CassandraQueryWaitStrategy by default to avoid potential issues when the authentication is enabled. waitingFor(new CassandraQueryWaitStrategy()); } @Override protected void configure() { // Map (effectively replace) directory in Docker with the content of resourceLocation if resource location is // not null. Optional .ofNullable(configLocation) .map(MountableFile::forClasspathResource) .ifPresent(mountableFile -> withCopyFileToContainer(mountableFile, CONTAINER_CONFIG_LOCATION)); // If a secure connection is required by Cassandra configuration, copy the user certificate and key to a // dedicated location and define a cqlshrc file with the appropriate SSL configuration. // See: https://docs.datastax.com/en/cassandra-oss/3.x/cassandra/configuration/secureCqlshSSL.html if (isSslRequired()) { withCopyFileToContainer(MountableFile.forClasspathResource(clientCertFile), "ssl/user_cert.pem"); withCopyFileToContainer(MountableFile.forClasspathResource(clientKeyFile), "ssl/user_key.pem"); withCopyFileToContainer(MountableFile.forClasspathResource("cqlshrc"), "/root/.cassandra/cqlshrc"); } } @Override protected void containerIsStarted(InspectContainerResponse containerInfo) { runInitScriptIfRequired(); } /** * Load init script content and apply it to the database if initScriptPath is set */ private void runInitScriptIfRequired() { if (this.initScriptPath != null) { try { final MountableFile originalInitScript = MountableFile.forClasspathResource(this.initScriptPath); // The init script is executed as is by the cqlsh command, so copy it into the container. The name // of the script is generic since it's not important to keep the original name. copyFileToContainer(originalInitScript, DEFAULT_INIT_SCRIPT_FILENAME); new CassandraDatabaseDelegate(this).execute(null, DEFAULT_INIT_SCRIPT_FILENAME, -1, false, false); } catch (IllegalArgumentException e) { // MountableFile.forClasspathResource will throw an IllegalArgumentException if the resource cannot // be found. logger().warn("Could not load classpath init script: {}", this.initScriptPath); throw new ScriptLoadException( "Could not load classpath init script: " + this.initScriptPath + ". Resource not found.", e ); } catch (ScriptUtils.ScriptStatementFailedException e) { logger().error("Error while executing init script: {}", this.initScriptPath, e); throw new ScriptUtils.UncategorizedScriptException( "Error while executing init script: " + this.initScriptPath, e ); } } } /** * Initialize Cassandra with the custom overridden Cassandra configuration *

* Be aware, that Docker effectively replaces all /etc/cassandra content with the content of config location, so if * Cassandra.yaml in configLocation is absent or corrupted, then Cassandra just won't launch. * * @param configLocation relative classpath with the directory that contains cassandra.yaml and other configuration * files * @return The updated {@link CassandraContainer}. */ public CassandraContainer withConfigurationOverride(String configLocation) { this.configLocation = configLocation; return self(); } /** * Initialize Cassandra with init CQL script *

* CQL script will be applied after container is started (see using WaitStrategy). *

* * @param initScriptPath relative classpath resource * @return The updated {@link CassandraContainer}. */ public CassandraContainer withInitScript(String initScriptPath) { this.initScriptPath = initScriptPath; return self(); } /** * Configure secured connection (TLS) when required by the Cassandra configuration * (i.e. cassandra.yaml file contains the property {@code client_encryption_options.optional} with value * {@code false}). * * @param clientCertFile The client certificate required to execute CQL scripts. * @param clientKeyFile The client key required to execute CQL scripts. * @return The updated {@link CassandraContainer}. */ public CassandraContainer withSsl(String clientCertFile, String clientKeyFile) { this.clientCertFile = clientCertFile; this.clientKeyFile = clientKeyFile; return self(); } /** * @return Whether a secure connection is required between the client and the Cassandra server. */ boolean isSslRequired() { return StringUtils.isNoneBlank(this.clientCertFile, this.clientKeyFile); } /** * Get username *

* By default, Cassandra has authenticator: AllowAllAuthenticator in cassandra.yaml * If username and password need to be used, then authenticator should be set as PasswordAuthenticator * (through custom Cassandra configuration) and through CQL with default cassandra-cassandra credentials * user management should be modified */ public String getUsername() { return USERNAME; } /** * Get password *

* By default, Cassandra has authenticator: AllowAllAuthenticator in cassandra.yaml * If username and password need to be used, then authenticator should be set as PasswordAuthenticator * (through custom Cassandra configuration) and through CQL with default cassandra-cassandra credentials * user management should be modified */ public String getPassword() { return PASSWORD; } /** * Retrieve an {@link InetSocketAddress} for connecting to the Cassandra container via the driver. * * @return A InetSocketAddress representation of this Cassandra container's host and port. */ public InetSocketAddress getContactPoint() { return new InetSocketAddress(getHost(), getMappedPort(CQL_PORT)); } /** * Retrieve the Local Datacenter for connecting to the Cassandra container via the driver. * * @return The configured local Datacenter name. */ public String getLocalDatacenter() { return getEnvMap().getOrDefault("CASSANDRA_DC", DEFAULT_LOCAL_DATACENTER); } } ================================================ FILE: modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraDatabaseDelegate.java ================================================ package org.testcontainers.cassandra; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.testcontainers.containers.Container; import org.testcontainers.containers.ContainerState; import org.testcontainers.containers.ExecConfig; import org.testcontainers.delegate.AbstractDatabaseDelegate; import org.testcontainers.ext.ScriptUtils.ScriptStatementFailedException; import java.io.IOException; /** * Cassandra database delegate */ @Slf4j @RequiredArgsConstructor public class CassandraDatabaseDelegate extends AbstractDatabaseDelegate { private final ContainerState container; @Override protected Void createNewConnection() { // Return null here, because we run scripts using cqlsh command directly in the container. // So, we don't use connection object in the execute() method. return null; } public void execute( String statement, String scriptPath, int lineNumber, boolean continueOnError, boolean ignoreFailedDrops, boolean silentErrorLogs ) { try { // Use cqlsh command directly inside the container to execute statements // See documentation here: https://cassandra.apache.org/doc/stable/cassandra/tools/cqlsh.html String[] cqlshCommand = new String[] { "cqlsh" }; if (this.container instanceof CassandraContainer) { CassandraContainer cassandraContainer = (CassandraContainer) this.container; String username = cassandraContainer.getUsername(); String password = cassandraContainer.getPassword(); if (cassandraContainer.isSslRequired()) { cqlshCommand = ArrayUtils.add(cqlshCommand, "--ssl"); } cqlshCommand = ArrayUtils.addAll(cqlshCommand, "-u", username, "-p", password); } // If no statement specified, directly execute the script specified into scriptPath (using -f argument), // otherwise execute the given statement (using -e argument). String executeArg = "-e"; String executeArgValue = statement; if (StringUtils.isBlank(statement)) { executeArg = "-f"; executeArgValue = scriptPath; } cqlshCommand = ArrayUtils.addAll(cqlshCommand, executeArg, executeArgValue); Container.ExecResult result = this.container.execInContainer(ExecConfig.builder().command(cqlshCommand).build()); if (result.getExitCode() == 0) { if (StringUtils.isBlank(statement)) { log.info("CQL script {} successfully executed", scriptPath); } else { log.info("CQL statement {} was applied", statement); } } else { if (!silentErrorLogs) { log.error("CQL script execution failed with error: \n{}", result.getStderr()); } throw new ScriptStatementFailedException(statement, lineNumber, scriptPath); } } catch (IOException | InterruptedException e) { throw new ScriptStatementFailedException(statement, lineNumber, scriptPath, e); } } @Override public void execute( String statement, String scriptPath, int lineNumber, boolean continueOnError, boolean ignoreFailedDrops ) { this.execute(statement, scriptPath, lineNumber, continueOnError, ignoreFailedDrops, false); } @Override protected void closeConnectionQuietly(Void session) { // Nothing to do here, because we run scripts using cqlsh command directly in the container. } } ================================================ FILE: modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraQueryWaitStrategy.java ================================================ package org.testcontainers.cassandra; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.rnorth.ducttape.TimeoutException; import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy; import org.testcontainers.delegate.DatabaseDelegate; import java.util.concurrent.TimeUnit; import static org.rnorth.ducttape.unreliables.Unreliables.retryUntilSuccess; /** * Waits until Cassandra returns its version */ @Slf4j public class CassandraQueryWaitStrategy extends AbstractWaitStrategy { private static final String SELECT_VERSION_QUERY = "SELECT release_version FROM system.local"; private static final String TIMEOUT_ERROR = "Timed out waiting for Cassandra to be accessible for query execution"; @Override protected void waitUntilReady() { // execute select version query until success or timeout try { retryUntilSuccess( (int) startupTimeout.getSeconds(), TimeUnit.SECONDS, () -> { getRateLimiter() .doWhenReady(() -> { try (DatabaseDelegate databaseDelegate = getDatabaseDelegate()) { log.info("Checking connection is ready..."); ((CassandraDatabaseDelegate) databaseDelegate).execute( SELECT_VERSION_QUERY, StringUtils.EMPTY, 1, false, false, true ); } }); return true; } ); } catch (TimeoutException e) { throw new ContainerLaunchException(TIMEOUT_ERROR); } } private DatabaseDelegate getDatabaseDelegate() { return new CassandraDatabaseDelegate(waitStrategyTarget); } } ================================================ FILE: modules/cassandra/src/main/java/org/testcontainers/containers/CassandraContainer.java ================================================ package org.testcontainers.containers; import com.datastax.driver.core.Cluster; import com.github.dockerjava.api.command.InspectContainerResponse; import org.apache.commons.io.IOUtils; import org.testcontainers.containers.delegate.CassandraDatabaseDelegate; import org.testcontainers.delegate.DatabaseDelegate; import org.testcontainers.ext.ScriptUtils; import org.testcontainers.ext.ScriptUtils.ScriptLoadException; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.io.IOException; import java.net.InetSocketAddress; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Optional; import javax.script.ScriptException; /** * Testcontainers implementation for Apache Cassandra. *

* Supported image: {@code cassandra} *

* Exposed ports: 9042 * * @deprecated use {@link org.testcontainers.cassandra.CassandraContainer} instead. */ @Deprecated public class CassandraContainer> extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("cassandra"); private static final String DEFAULT_TAG = "3.11.2"; @Deprecated public static final String IMAGE = DEFAULT_IMAGE_NAME.getUnversionedPart(); public static final Integer CQL_PORT = 9042; private static final String DEFAULT_LOCAL_DATACENTER = "datacenter1"; private static final String CONTAINER_CONFIG_LOCATION = "/etc/cassandra"; private static final String USERNAME = "cassandra"; private static final String PASSWORD = "cassandra"; private String configLocation; private String initScriptPath; private boolean enableJmxReporting; /** * @deprecated use {@link #CassandraContainer(DockerImageName)} instead */ @Deprecated public CassandraContainer() { this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } public CassandraContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public CassandraContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); addExposedPort(CQL_PORT); this.enableJmxReporting = false; withEnv("CASSANDRA_SNITCH", "GossipingPropertyFileSnitch"); withEnv("JVM_OPTS", "-Dcassandra.skip_wait_for_gossip_to_settle=0 -Dcassandra.initial_token=0"); withEnv("HEAP_NEWSIZE", "128M"); withEnv("MAX_HEAP_SIZE", "1024M"); withEnv("CASSANDRA_ENDPOINT_SNITCH", "GossipingPropertyFileSnitch"); withEnv("CASSANDRA_DC", DEFAULT_LOCAL_DATACENTER); } @Override protected void configure() { optionallyMapResourceParameterAsVolume(CONTAINER_CONFIG_LOCATION, configLocation); } @Override protected void containerIsStarted(InspectContainerResponse containerInfo) { runInitScriptIfRequired(); } /** * Load init script content and apply it to the database if initScriptPath is set */ private void runInitScriptIfRequired() { if (initScriptPath != null) { try { URL resource = Thread.currentThread().getContextClassLoader().getResource(initScriptPath); if (resource == null) { logger().warn("Could not load classpath init script: {}", initScriptPath); throw new ScriptLoadException( "Could not load classpath init script: " + initScriptPath + ". Resource not found." ); } String cql = IOUtils.toString(resource, StandardCharsets.UTF_8); DatabaseDelegate databaseDelegate = getDatabaseDelegate(); ScriptUtils.executeDatabaseScript(databaseDelegate, initScriptPath, cql); } catch (IOException e) { logger().warn("Could not load classpath init script: {}", initScriptPath); throw new ScriptLoadException("Could not load classpath init script: " + initScriptPath, e); } catch (ScriptException e) { logger().error("Error while executing init script: {}", initScriptPath, e); throw new ScriptUtils.UncategorizedScriptException( "Error while executing init script: " + initScriptPath, e ); } } } /** * Map (effectively replace) directory in Docker with the content of resourceLocation if resource location is not null * * Protected to allow for changing implementation by extending the class * * @param pathNameInContainer path in docker * @param resourceLocation relative classpath to resource */ protected void optionallyMapResourceParameterAsVolume(String pathNameInContainer, String resourceLocation) { Optional .ofNullable(resourceLocation) .map(MountableFile::forClasspathResource) .ifPresent(mountableFile -> withCopyFileToContainer(mountableFile, pathNameInContainer)); } /** * Initialize Cassandra with the custom overridden Cassandra configuration *

* Be aware, that Docker effectively replaces all /etc/cassandra content with the content of config location, so if * Cassandra.yaml in configLocation is absent or corrupted, then Cassandra just won't launch * * @param configLocation relative classpath with the directory that contains cassandra.yaml and other configuration files */ public SELF withConfigurationOverride(String configLocation) { this.configLocation = configLocation; return self(); } /** * Initialize Cassandra with init CQL script *

* CQL script will be applied after container is started (see using WaitStrategy) * * @param initScriptPath relative classpath resource */ public SELF withInitScript(String initScriptPath) { this.initScriptPath = initScriptPath; return self(); } /** * Initialize Cassandra client with JMX reporting enabled or disabled */ public SELF withJmxReporting(boolean enableJmxReporting) { this.enableJmxReporting = enableJmxReporting; return self(); } /** * Get username * * By default Cassandra has authenticator: AllowAllAuthenticator in cassandra.yaml * If username and password need to be used, then authenticator should be set as PasswordAuthenticator * (through custom Cassandra configuration) and through CQL with default cassandra-cassandra credentials * user management should be modified */ public String getUsername() { return USERNAME; } /** * Get password * * By default Cassandra has authenticator: AllowAllAuthenticator in cassandra.yaml * If username and password need to be used, then authenticator should be set as PasswordAuthenticator * (through custom Cassandra configuration) and through CQL with default cassandra-cassandra credentials * user management should be modified */ public String getPassword() { return PASSWORD; } /** * Get configured Cluster * * Can be used to obtain connections to Cassandra in the container * * @deprecated For Cassandra driver 3.x, use {@link #getHost()} and {@link #getMappedPort(int)} with * the driver's {@link Cluster#builder() Cluster.Builder} {@code addContactPoint(String)} and * {@code withPort(int)} methods to create a Cluster object. For Cassandra driver 4.x, use * {@link #getContactPoint()} and {@link #getLocalDatacenter()} with the driver's {@code CqlSession.builder()} * {@code addContactPoint(InetSocketAddress)} and {@code withLocalDatacenter(String)} methods to create * a Session Object. See https://docs.datastax.com/en/developer/java-driver/ for more on the driver. */ @Deprecated public Cluster getCluster() { return getCluster(this, enableJmxReporting); } @Deprecated public static Cluster getCluster(ContainerState containerState, boolean enableJmxReporting) { final Cluster.Builder builder = Cluster .builder() .addContactPoint(containerState.getHost()) .withPort(containerState.getMappedPort(CQL_PORT)); if (!enableJmxReporting) { builder.withoutJMXReporting(); } return builder.build(); } @Deprecated public static Cluster getCluster(ContainerState containerState) { return getCluster(containerState, false); } /** * Retrieve an {@link InetSocketAddress} for connecting to the Cassandra container via the driver. * * @return A InetSocketAddress representation of this Cassandra container's host and port. */ public InetSocketAddress getContactPoint() { return new InetSocketAddress(getHost(), getMappedPort(CQL_PORT)); } /** * Retrieve the Local Datacenter for connecting to the Cassandra container via the driver. * * @return The configured local Datacenter name. */ public String getLocalDatacenter() { return getEnvMap().getOrDefault("CASSANDRA_DC", DEFAULT_LOCAL_DATACENTER); } @Deprecated private DatabaseDelegate getDatabaseDelegate() { return new CassandraDatabaseDelegate(this); } } ================================================ FILE: modules/cassandra/src/main/java/org/testcontainers/containers/delegate/CassandraDatabaseDelegate.java ================================================ package org.testcontainers.containers.delegate; import com.datastax.driver.core.ResultSet; import com.datastax.driver.core.Session; import com.datastax.driver.core.exceptions.DriverException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.testcontainers.containers.CassandraContainer; import org.testcontainers.containers.ContainerState; import org.testcontainers.delegate.AbstractDatabaseDelegate; import org.testcontainers.exception.ConnectionCreationException; import org.testcontainers.ext.ScriptUtils.ScriptStatementFailedException; /** * Cassandra database delegate * * @deprecated use {@link org.testcontainers.cassandra.CassandraDatabaseDelegate} instead. */ @Slf4j @RequiredArgsConstructor @Deprecated public class CassandraDatabaseDelegate extends AbstractDatabaseDelegate { private final ContainerState container; @Override protected Session createNewConnection() { try { return CassandraContainer.getCluster(container).newSession(); } catch (DriverException e) { log.error("Could not obtain cassandra connection"); throw new ConnectionCreationException("Could not obtain cassandra connection", e); } } @Override public void execute( String statement, String scriptPath, int lineNumber, boolean continueOnError, boolean ignoreFailedDrops ) { try { ResultSet result = getConnection().execute(statement); if (result.wasApplied()) { log.debug("Statement {} was applied", statement); } else { throw new ScriptStatementFailedException(statement, lineNumber, scriptPath); } } catch (DriverException e) { throw new ScriptStatementFailedException(statement, lineNumber, scriptPath, e); } } @Override protected void closeConnectionQuietly(Session session) { try { session.getCluster().close(); } catch (Exception e) { log.error("Could not close cassandra connection", e); } } } ================================================ FILE: modules/cassandra/src/main/java/org/testcontainers/containers/wait/CassandraQueryWaitStrategy.java ================================================ package org.testcontainers.containers.wait; import org.rnorth.ducttape.TimeoutException; import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.containers.delegate.CassandraDatabaseDelegate; import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy; import org.testcontainers.delegate.DatabaseDelegate; import java.util.concurrent.TimeUnit; import static org.rnorth.ducttape.unreliables.Unreliables.retryUntilSuccess; /** * Waits until Cassandra returns its version * * @deprecated use {@link org.testcontainers.cassandra.CassandraQueryWaitStrategy} instead. */ @Deprecated public class CassandraQueryWaitStrategy extends AbstractWaitStrategy { private static final String SELECT_VERSION_QUERY = "SELECT release_version FROM system.local"; private static final String TIMEOUT_ERROR = "Timed out waiting for Cassandra to be accessible for query execution"; @Override protected void waitUntilReady() { // execute select version query until success or timeout try { retryUntilSuccess( (int) startupTimeout.getSeconds(), TimeUnit.SECONDS, () -> { getRateLimiter() .doWhenReady(() -> { try (DatabaseDelegate databaseDelegate = getDatabaseDelegate()) { databaseDelegate.execute(SELECT_VERSION_QUERY, "", 1, false, false); } }); return true; } ); } catch (TimeoutException e) { throw new ContainerLaunchException(TIMEOUT_ERROR); } } private DatabaseDelegate getDatabaseDelegate() { return new CassandraDatabaseDelegate(waitStrategyTarget); } } ================================================ FILE: modules/cassandra/src/main/resources/cqlshrc ================================================ [ssl] certfile = ssl/user_cert.pem usercert = ssl/user_cert.pem userkey = ssl/user_key.pem [connection] factory = cqlshlib.ssl.ssl_transport_factory ================================================ FILE: modules/cassandra/src/test/java/org/testcontainers/cassandra/CassandraContainerTest.java ================================================ package org.testcontainers.cassandra; import com.datastax.oss.driver.api.core.CqlSession; import com.datastax.oss.driver.api.core.CqlSessionBuilder; import com.datastax.oss.driver.api.core.config.DefaultDriverOption; import com.datastax.oss.driver.api.core.config.DriverConfigLoader; import com.datastax.oss.driver.api.core.config.ProgrammaticDriverConfigLoaderBuilder; import com.datastax.oss.driver.api.core.context.DriverContext; import com.datastax.oss.driver.api.core.cql.ResultSet; import com.datastax.oss.driver.api.core.cql.Row; import com.datastax.oss.driver.api.core.session.ProgrammaticArguments; import com.datastax.oss.driver.internal.core.context.DefaultDriverContext; import com.datastax.oss.driver.internal.core.ssl.DefaultSslEngineFactory; import org.junit.jupiter.api.Test; import org.testcontainers.containers.Container; import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.utility.DockerImageName; import java.net.URL; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.fail; class CassandraContainerTest { private static final String CASSANDRA_IMAGE = "cassandra:3.11.15"; private static final String TEST_CLUSTER_NAME_IN_CONF = "Test Cluster Integration Test"; private static final String BASIC_QUERY = "SELECT release_version FROM system.local"; @Test void testSimple() { try ( // container-definition { CassandraContainer cassandraContainer = new CassandraContainer(CASSANDRA_IMAGE) // } ) { cassandraContainer.start(); ResultSet resultSet = performQuery(cassandraContainer, BASIC_QUERY); assertThat(resultSet.wasApplied()).as("Query was applied").isTrue(); assertThat(resultSet.one().getString(0)).as("Result set has release_version").isNotNull(); } } @Test void testSpecificVersion() { String cassandraVersion = "3.0.15"; try ( CassandraContainer cassandraContainer = new CassandraContainer( DockerImageName.parse("cassandra").withTag(cassandraVersion) ) ) { cassandraContainer.start(); ResultSet resultSet = performQuery(cassandraContainer, BASIC_QUERY); assertThat(resultSet.wasApplied()).as("Query was applied").isTrue(); assertThat(resultSet.one().getString(0)).as("Cassandra has right version").isEqualTo(cassandraVersion); } } @Test void testConfigurationOverride() { try ( CassandraContainer cassandraContainer = new CassandraContainer(CASSANDRA_IMAGE) .withConfigurationOverride("cassandra-test-configuration-example") ) { cassandraContainer.start(); ResultSet resultSet = performQuery(cassandraContainer, "SELECT cluster_name FROM system.local"); assertThat(resultSet.wasApplied()).as("Query was applied").isTrue(); assertThat(resultSet.one().getString(0)) .as("Cassandra configuration is overridden") .isEqualTo(TEST_CLUSTER_NAME_IN_CONF); } } @Test public void testWithSslClientConfig() { /* Commands executed to generate certificates in 'cassandra-ssl-configuration' directory: keytool -genkey -keyalg RSA -validity 36500 -alias localhost -keystore keystore.p12 -storepass cassandra \ -keypass cassandra -dname "CN=localhost, OU=Testcontainers, O=Testcontainers, L=None, C=None" keytool -export -alias localhost -file cassandra.cer -keystore keystore.p12 keytool -import -v -trustcacerts -alias localhost -file cassandra.cer -keystore truststore.p12 Commands executed to generate the client certificate and key in 'client-ssl' directory: keytool -importkeystore -srckeystore keystore.p12 -destkeystore test_node.p12 -deststoretype PKCS12 \ -srcstorepass cassandra -deststorepass cassandra openssl pkcs12 -in test_node.p12 -nokeys -out cassandra.cer.pem -passin pass:cassandra openssl pkcs12 -in test_node.p12 -nodes -nocerts -out cassandra.key.pem -passin pass:cassandra Reference: https://docs.datastax.com/en/cassandra-oss/3.x/cassandra/configuration/secureSSLCertificates.html https://docs.datastax.com/en/cassandra-oss/3.x/cassandra/configuration/secureCqlshSSL.html */ try ( // with-ssl-config { CassandraContainer cassandraContainer = new CassandraContainer(CASSANDRA_IMAGE) .withConfigurationOverride("cassandra-ssl-configuration") .withSsl("client-ssl/cassandra.cer.pem", "client-ssl/cassandra.key.pem") // } ) { cassandraContainer.start(); try { ResultSet resultSet = performQueryWithSslClientConfig( cassandraContainer, "SELECT cluster_name FROM system.local" ); assertThat(resultSet.wasApplied()).as("Query was applied").isTrue(); assertThat(resultSet.one().getString(0)) .as("Cassandra configuration is configured with secured connection") .isEqualTo(TEST_CLUSTER_NAME_IN_CONF); } catch (Exception e) { fail(e); } } } @Test public void testSimpleSslCqlsh() { try ( CassandraContainer cassandraContainer = new CassandraContainer(CASSANDRA_IMAGE) .withConfigurationOverride("cassandra-ssl-configuration") .withSsl("client-ssl/cassandra.cer.pem", "client-ssl/cassandra.key.pem") ) { cassandraContainer.start(); Container.ExecResult execResult = cassandraContainer.execInContainer( "cqlsh", "--ssl", "-e", "SELECT * FROM system_schema.keyspaces;" ); assertThat(execResult.getStdout()).contains("keyspace_name"); } catch (Exception e) { fail(e); } } @Test void testEmptyConfigurationOverride() { try ( CassandraContainer cassandraContainer = new CassandraContainer(CASSANDRA_IMAGE) .withConfigurationOverride("cassandra-empty-configuration") ) { assertThatThrownBy(cassandraContainer::start).isInstanceOf(ContainerLaunchException.class); } } @Test void testInitScript() { try ( CassandraContainer cassandraContainer = new CassandraContainer(CASSANDRA_IMAGE) .withInitScript("initial.cql") ) { cassandraContainer.start(); testInitScript(cassandraContainer, false); } } @Test void testNonexistentInitScript() { try ( CassandraContainer cassandraContainer = new CassandraContainer(CASSANDRA_IMAGE) .withInitScript("unknown_script.cql") ) { assertThatThrownBy(cassandraContainer::start).isInstanceOf(ContainerLaunchException.class); } } @Test void testInitScriptWithRequiredAuthentication() { try ( // init-with-auth { CassandraContainer cassandraContainer = new CassandraContainer(CASSANDRA_IMAGE) .withConfigurationOverride("cassandra-auth-required-configuration") .withInitScript("initial.cql") // } ) { cassandraContainer.start(); testInitScript(cassandraContainer, true); } } @Test void testInitScriptWithError() { try ( CassandraContainer cassandraContainer = new CassandraContainer(CASSANDRA_IMAGE) .withInitScript("initial-with-error.cql") ) { assertThatThrownBy(cassandraContainer::start).isInstanceOf(ContainerLaunchException.class); } } @Test void testInitScriptWithLegacyCassandra() { try ( CassandraContainer cassandraContainer = new CassandraContainer("cassandra:2.2.11") .withInitScript("initial.cql") ) { cassandraContainer.start(); testInitScript(cassandraContainer, false); } } private void testInitScript(CassandraContainer cassandraContainer, boolean withCredentials) { String query = "SELECT * FROM keySpaceTest.catalog_category"; ResultSet resultSet; if (withCredentials) { resultSet = performQueryWithAuth(cassandraContainer, query); } else { resultSet = performQuery(cassandraContainer, query); } assertThat(resultSet.wasApplied()).as("Query was applied").isTrue(); Row row = resultSet.one(); assertThat(row.getLong(0)).as("Inserted row is in expected state").isEqualTo(1); assertThat(row.getString(1)).as("Inserted row is in expected state").isEqualTo("test_category"); } private ResultSet performQuery(CassandraContainer cassandraContainer, String cql) { // cql-session { final CqlSession cqlSession = CqlSession .builder() .addContactPoint(cassandraContainer.getContactPoint()) .withLocalDatacenter(cassandraContainer.getLocalDatacenter()) .build(); // } return performQuery(cqlSession, cql); } private ResultSet performQueryWithAuth(CassandraContainer cassandraContainer, String cql) { final CqlSession cqlSession = CqlSession .builder() .addContactPoint(cassandraContainer.getContactPoint()) .withLocalDatacenter(cassandraContainer.getLocalDatacenter()) .withAuthCredentials(cassandraContainer.getUsername(), cassandraContainer.getPassword()) .build(); return performQuery(cqlSession, cql); } private ResultSet performQueryWithSslClientConfig(CassandraContainer cassandraContainer, String cql) { final ProgrammaticDriverConfigLoaderBuilder driverConfigLoaderBuilder = DriverConfigLoader.programmaticBuilder(); driverConfigLoaderBuilder.withBoolean(DefaultDriverOption.SSL_HOSTNAME_VALIDATION, false); final URL trustStoreUrl = this.getClass().getClassLoader().getResource("cassandra-ssl-configuration/truststore.p12"); driverConfigLoaderBuilder.withString(DefaultDriverOption.SSL_TRUSTSTORE_PATH, trustStoreUrl.getFile()); driverConfigLoaderBuilder.withString(DefaultDriverOption.SSL_TRUSTSTORE_PASSWORD, "cassandra"); final URL keyStoreUrl = this.getClass().getClassLoader().getResource("cassandra-ssl-configuration/keystore.p12"); driverConfigLoaderBuilder.withString(DefaultDriverOption.SSL_KEYSTORE_PATH, keyStoreUrl.getFile()); driverConfigLoaderBuilder.withString(DefaultDriverOption.SSL_KEYSTORE_PASSWORD, "cassandra"); final DriverContext driverContext = new DefaultDriverContext( driverConfigLoaderBuilder.build(), ProgrammaticArguments.builder().build() ); final CqlSessionBuilder sessionBuilder = CqlSession.builder(); final CqlSession cqlSession = sessionBuilder .addContactPoint(cassandraContainer.getContactPoint()) .withLocalDatacenter(cassandraContainer.getLocalDatacenter()) .withSslEngineFactory(new DefaultSslEngineFactory(driverContext)) .build(); return performQuery(cqlSession, cql); } private ResultSet performQuery(CqlSession session, String cql) { final ResultSet rs = session.execute(cql); session.close(); return rs; } } ================================================ FILE: modules/cassandra/src/test/java/org/testcontainers/cassandra/CompatibleCassandraImageTest.java ================================================ package org.testcontainers.cassandra; import com.datastax.oss.driver.api.core.CqlIdentifier; import com.datastax.oss.driver.api.core.CqlSession; import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import static org.assertj.core.api.Assertions.assertThat; class CompatibleCassandraImageTest { public static String[] params() { return new String[] { "cassandra:3.11.2", "cassandra:4.1.1", "cassandra:5" }; } @ParameterizedTest @MethodSource("params") void testCassandraGetContactPoint(String imageName) { try (CassandraContainer cassandra = new CassandraContainer(imageName)) { cassandra.start(); assertCassandraFunctionality(cassandra); } } private void assertCassandraFunctionality(CassandraContainer cassandra) { try ( CqlSession session = CqlSession .builder() .addContactPoint(cassandra.getContactPoint()) .withLocalDatacenter(cassandra.getLocalDatacenter()) .build() ) { session.execute( "CREATE KEYSPACE IF NOT EXISTS test WITH replication = \n" + "{'class':'SimpleStrategy','replication_factor':'1'};" ); KeyspaceMetadata keyspace = session.getMetadata().getKeyspaces().get(CqlIdentifier.fromCql("test")); assertThat(keyspace).as("test keyspace created").isNotNull(); } } } ================================================ FILE: modules/cassandra/src/test/java/org/testcontainers/containers/CassandraContainerTest.java ================================================ package org.testcontainers.containers; import com.datastax.driver.core.Cluster; import com.datastax.driver.core.ResultSet; import com.datastax.driver.core.Row; import com.datastax.driver.core.Session; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.testcontainers.containers.wait.CassandraQueryWaitStrategy; import org.testcontainers.utility.DockerImageName; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @Slf4j class CassandraContainerTest { private static final DockerImageName CASSANDRA_IMAGE = DockerImageName.parse("cassandra:3.11.2"); private static final String TEST_CLUSTER_NAME_IN_CONF = "Test Cluster Integration Test"; private static final String BASIC_QUERY = "SELECT release_version FROM system.local"; @Test void testSimple() { try (CassandraContainer cassandraContainer = new CassandraContainer<>(CASSANDRA_IMAGE)) { cassandraContainer.start(); ResultSet resultSet = performQuery(cassandraContainer, BASIC_QUERY); assertThat(resultSet.wasApplied()).as("Query was applied").isTrue(); assertThat(resultSet.one().getString(0)).as("Result set has release_version").isNotNull(); } } @Test void testSpecificVersion() { String cassandraVersion = "3.0.15"; try ( CassandraContainer cassandraContainer = new CassandraContainer<>( CASSANDRA_IMAGE.withTag(cassandraVersion) ) ) { cassandraContainer.start(); ResultSet resultSet = performQuery(cassandraContainer, BASIC_QUERY); assertThat(resultSet.wasApplied()).as("Query was applied").isTrue(); assertThat(resultSet.one().getString(0)).as("Cassandra has right version").isEqualTo(cassandraVersion); } } @Test void testConfigurationOverride() { try ( CassandraContainer cassandraContainer = new CassandraContainer<>(CASSANDRA_IMAGE) .withConfigurationOverride("cassandra-test-configuration-example") ) { cassandraContainer.start(); ResultSet resultSet = performQuery(cassandraContainer, "SELECT cluster_name FROM system.local"); assertThat(resultSet.wasApplied()).as("Query was applied").isTrue(); assertThat(resultSet.one().getString(0)) .as("Cassandra configuration is overridden") .isEqualTo(TEST_CLUSTER_NAME_IN_CONF); } } @Test void testEmptyConfigurationOverride() { try ( CassandraContainer cassandraContainer = new CassandraContainer<>(CASSANDRA_IMAGE) .withConfigurationOverride("cassandra-empty-configuration") ) { assertThatThrownBy(cassandraContainer::start).isInstanceOf(ContainerLaunchException.class); } } @Test void testInitScript() { try ( CassandraContainer cassandraContainer = new CassandraContainer<>(CASSANDRA_IMAGE) .withInitScript("initial.cql") ) { cassandraContainer.start(); testInitScript(cassandraContainer); } } @Test void testInitScriptWithLegacyCassandra() { try ( CassandraContainer cassandraContainer = new CassandraContainer<>( DockerImageName.parse("cassandra:2.2.11") ) .withInitScript("initial.cql") ) { cassandraContainer.start(); testInitScript(cassandraContainer); } } @SuppressWarnings("deprecation") // Using deprecated constructor for verification of backwards compatibility @Test void testCassandraQueryWaitStrategy() { try ( CassandraContainer cassandraContainer = new CassandraContainer<>() .waitingFor(new CassandraQueryWaitStrategy()) ) { cassandraContainer.start(); ResultSet resultSet = performQuery(cassandraContainer, BASIC_QUERY); assertThat(resultSet.wasApplied()).as("Query was applied").isTrue(); } } @SuppressWarnings("deprecation") // Using deprecated constructor for verification of backwards compatibility @Test void testCassandraGetCluster() { try (CassandraContainer cassandraContainer = new CassandraContainer<>()) { cassandraContainer.start(); ResultSet resultSet = performQuery(cassandraContainer.getCluster(), BASIC_QUERY); assertThat(resultSet.wasApplied()).as("Query was applied").isTrue(); assertThat(resultSet.one().getString(0)).as("Result set has release_version").isNotNull(); } } private void testInitScript(CassandraContainer cassandraContainer) { ResultSet resultSet = performQuery(cassandraContainer, "SELECT * FROM keySpaceTest.catalog_category"); assertThat(resultSet.wasApplied()).as("Query was applied").isTrue(); Row row = resultSet.one(); assertThat(row.getLong(0)).as("Inserted row is in expected state").isEqualTo(1); assertThat(row.getString(1)).as("Inserted row is in expected state").isEqualTo("test_category"); } private ResultSet performQuery(CassandraContainer cassandraContainer, String cql) { Cluster explicitCluster = Cluster .builder() .addContactPoint(cassandraContainer.getHost()) .withPort(cassandraContainer.getMappedPort(CassandraContainer.CQL_PORT)) .build(); return performQuery(explicitCluster, cql); } private ResultSet performQuery(Cluster cluster, String cql) { try (Cluster closeableCluster = cluster) { Session session = closeableCluster.newSession(); return session.execute(cql); } } } ================================================ FILE: modules/cassandra/src/test/java/org/testcontainers/containers/CompatibleCassandraImageTest.java ================================================ package org.testcontainers.containers; import com.datastax.oss.driver.api.core.CqlIdentifier; import com.datastax.oss.driver.api.core.CqlSession; import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import static org.assertj.core.api.Assertions.assertThat; public class CompatibleCassandraImageTest { public static String[] params() { return new String[] { "cassandra:3.11.2", "cassandra:4.1.1" }; } @ParameterizedTest @MethodSource("params") void testCassandraGetContactPoint(String imageName) { try (CassandraContainer cassandra = new CassandraContainer<>(imageName)) { cassandra.start(); assertCassandraFunctionality(cassandra); } } private void assertCassandraFunctionality(CassandraContainer cassandra) { try ( CqlSession session = CqlSession .builder() .addContactPoint(cassandra.getContactPoint()) .withLocalDatacenter(cassandra.getLocalDatacenter()) .build() ) { session.execute( "CREATE KEYSPACE IF NOT EXISTS test WITH replication = \n" + "{'class':'SimpleStrategy','replication_factor':'1'};" ); KeyspaceMetadata keyspace = session.getMetadata().getKeyspaces().get(CqlIdentifier.fromCql("test")); assertThat(keyspace).as("test keyspace created").isNotNull(); } } } ================================================ FILE: modules/cassandra/src/test/resources/cassandra-auth-required-configuration/cassandra.yaml ================================================ # Cassandra storage config YAML # NOTE: # See http://wiki.apache.org/cassandra/StorageConfiguration for # full explanations of configuration directives # /NOTE # The name of the cluster. This is mainly used to prevent machines in # one logical cluster from joining another. cluster_name: 'Test Cluster Integration Test' # This defines the number of tokens randomly assigned to this node on the ring # The more tokens, relative to other nodes, the larger the proportion of data # that this node will store. You probably want all nodes to have the same number # of tokens assuming they have equal hardware capability. # # If you leave this unspecified, Cassandra will use the default of 1 token for legacy compatibility, # and will use the initial_token as described below. # # Specifying initial_token will override this setting on the node's initial start, # on subsequent starts, this setting will apply even if initial token is set. # # If you already have a cluster with 1 token per node, and wish to migrate to # multiple tokens per node, see http://wiki.apache.org/cassandra/Operations num_tokens: 256 # Triggers automatic allocation of num_tokens tokens for this node. The allocation # algorithm attempts to choose tokens in a way that optimizes replicated load over # the nodes in the datacenter for the replication strategy used by the specified # keyspace. # # The load assigned to each node will be close to proportional to its number of # vnodes. # # Only supported with the Murmur3Partitioner. # allocate_tokens_for_keyspace: KEYSPACE # initial_token allows you to specify tokens manually. While you can use it with # vnodes (num_tokens > 1, above) -- in which case you should provide a # comma-separated list -- it's primarily used when adding nodes to legacy clusters # that do not have vnodes enabled. # initial_token: # See http://wiki.apache.org/cassandra/HintedHandoff # May either be "true" or "false" to enable globally hinted_handoff_enabled: true # When hinted_handoff_enabled is true, a black list of data centers that will not # perform hinted handoff # hinted_handoff_disabled_datacenters: # - DC1 # - DC2 # this defines the maximum amount of time a dead host will have hints # generated. After it has been dead this long, new hints for it will not be # created until it has been seen alive and gone down again. max_hint_window_in_ms: 10800000 # 3 hours # Maximum throttle in KBs per second, per delivery thread. This will be # reduced proportionally to the number of nodes in the cluster. (If there # are two nodes in the cluster, each delivery thread will use the maximum # rate; if there are three, each will throttle to half of the maximum, # since we expect two nodes to be delivering hints simultaneously.) hinted_handoff_throttle_in_kb: 1024 # Number of threads with which to deliver hints; # Consider increasing this number when you have multi-dc deployments, since # cross-dc handoff tends to be slower max_hints_delivery_threads: 2 # Directory where Cassandra should store hints. # If not set, the default directory is $CASSANDRA_HOME/data/hints. # hints_directory: /var/lib/cassandra/hints # How often hints should be flushed from the internal buffers to disk. # Will *not* trigger fsync. hints_flush_period_in_ms: 10000 # Maximum size for a single hints file, in megabytes. max_hints_file_size_in_mb: 128 # Compression to apply to the hint files. If omitted, hints files # will be written uncompressed. LZ4, Snappy, and Deflate compressors # are supported. #hints_compression: # - class_name: LZ4Compressor # parameters: # - # Maximum throttle in KBs per second, total. This will be # reduced proportionally to the number of nodes in the cluster. batchlog_replay_throttle_in_kb: 1024 # Authentication backend, implementing IAuthenticator; used to identify users # Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthenticator, # PasswordAuthenticator}. # # - AllowAllAuthenticator performs no checks - set it to disable authentication. # - PasswordAuthenticator relies on username/password pairs to authenticate # users. It keeps usernames and hashed passwords in system_auth.roles table. # Please increase system_auth keyspace replication factor if you use this authenticator. # If using PasswordAuthenticator, CassandraRoleManager must also be used (see below) authenticator: PasswordAuthenticator # Authorization backend, implementing IAuthorizer; used to limit access/provide permissions # Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthorizer, # CassandraAuthorizer}. # # - AllowAllAuthorizer allows any action to any user - set it to disable authorization. # - CassandraAuthorizer stores permissions in system_auth.role_permissions table. Please # increase system_auth keyspace replication factor if you use this authorizer. authorizer: AllowAllAuthorizer # Part of the Authentication & Authorization backend, implementing IRoleManager; used # to maintain grants and memberships between roles. # Out of the box, Cassandra provides org.apache.cassandra.auth.CassandraRoleManager, # which stores role information in the system_auth keyspace. Most functions of the # IRoleManager require an authenticated login, so unless the configured IAuthenticator # actually implements authentication, most of this functionality will be unavailable. # # - CassandraRoleManager stores role data in the system_auth keyspace. Please # increase system_auth keyspace replication factor if you use this role manager. role_manager: CassandraRoleManager # Validity period for roles cache (fetching granted roles can be an expensive # operation depending on the role manager, CassandraRoleManager is one example) # Granted roles are cached for authenticated sessions in AuthenticatedUser and # after the period specified here, become eligible for (async) reload. # Defaults to 2000, set to 0 to disable caching entirely. # Will be disabled automatically for AllowAllAuthenticator. roles_validity_in_ms: 2000 # Refresh interval for roles cache (if enabled). # After this interval, cache entries become eligible for refresh. Upon next # access, an async reload is scheduled and the old value returned until it # completes. If roles_validity_in_ms is non-zero, then this must be # also. # Defaults to the same value as roles_validity_in_ms. # roles_update_interval_in_ms: 2000 # Validity period for permissions cache (fetching permissions can be an # expensive operation depending on the authorizer, CassandraAuthorizer is # one example). Defaults to 2000, set to 0 to disable. # Will be disabled automatically for AllowAllAuthorizer. permissions_validity_in_ms: 2000 # Refresh interval for permissions cache (if enabled). # After this interval, cache entries become eligible for refresh. Upon next # access, an async reload is scheduled and the old value returned until it # completes. If permissions_validity_in_ms is non-zero, then this must be # also. # Defaults to the same value as permissions_validity_in_ms. # permissions_update_interval_in_ms: 2000 # Validity period for credentials cache. This cache is tightly coupled to # the provided PasswordAuthenticator implementation of IAuthenticator. If # another IAuthenticator implementation is configured, this cache will not # be automatically used and so the following settings will have no effect. # Please note, credentials are cached in their encrypted form, so while # activating this cache may reduce the number of queries made to the # underlying table, it may not bring a significant reduction in the # latency of individual authentication attempts. # Defaults to 2000, set to 0 to disable credentials caching. credentials_validity_in_ms: 2000 # Refresh interval for credentials cache (if enabled). # After this interval, cache entries become eligible for refresh. Upon next # access, an async reload is scheduled and the old value returned until it # completes. If credentials_validity_in_ms is non-zero, then this must be # also. # Defaults to the same value as credentials_validity_in_ms. # credentials_update_interval_in_ms: 2000 # The partitioner is responsible for distributing groups of rows (by # partition key) across nodes in the cluster. You should leave this # alone for new clusters. The partitioner can NOT be changed without # reloading all data, so when upgrading you should set this to the # same partitioner you were already using. # # Besides Murmur3Partitioner, partitioners included for backwards # compatibility include RandomPartitioner, ByteOrderedPartitioner, and # OrderPreservingPartitioner. # partitioner: org.apache.cassandra.dht.Murmur3Partitioner # Directories where Cassandra should store data on disk. Cassandra # will spread data evenly across them, subject to the granularity of # the configured compaction strategy. # If not set, the default directory is $CASSANDRA_HOME/data/data. data_file_directories: - /var/lib/cassandra/data # commit log. when running on magnetic HDD, this should be a # separate spindle than the data directories. # If not set, the default directory is $CASSANDRA_HOME/data/commitlog. commitlog_directory: /var/lib/cassandra/commitlog # Enable / disable CDC functionality on a per-node basis. This modifies the logic used # for write path allocation rejection (standard: never reject. cdc: reject Mutation # containing a CDC-enabled table if at space limit in cdc_raw_directory). cdc_enabled: false # CommitLogSegments are moved to this directory on flush if cdc_enabled: true and the # segment contains mutations for a CDC-enabled table. This should be placed on a # separate spindle than the data directories. If not set, the default directory is # $CASSANDRA_HOME/data/cdc_raw. # cdc_raw_directory: /var/lib/cassandra/cdc_raw # Policy for data disk failures: # # die # shut down gossip and client transports and kill the JVM for any fs errors or # single-sstable errors, so the node can be replaced. # # stop_paranoid # shut down gossip and client transports even for single-sstable errors, # kill the JVM for errors during startup. # # stop # shut down gossip and client transports, leaving the node effectively dead, but # can still be inspected via JMX, kill the JVM for errors during startup. # # best_effort # stop using the failed disk and respond to requests based on # remaining available sstables. This means you WILL see obsolete # data at CL.ONE! # # ignore # ignore fatal errors and let requests fail, as in pre-1.2 Cassandra disk_failure_policy: stop # Policy for commit disk failures: # # die # shut down gossip and Thrift and kill the JVM, so the node can be replaced. # # stop # shut down gossip and Thrift, leaving the node effectively dead, but # can still be inspected via JMX. # # stop_commit # shutdown the commit log, letting writes collect but # continuing to service reads, as in pre-2.0.5 Cassandra # # ignore # ignore fatal errors and let the batches fail commit_failure_policy: stop # Maximum size of the native protocol prepared statement cache # # Valid values are either "auto" (omitting the value) or a value greater 0. # # Note that specifying a too large value will result in long running GCs and possibly # out-of-memory errors. Keep the value at a small fraction of the heap. # # If you constantly see "prepared statements discarded in the last minute because # cache limit reached" messages, the first step is to investigate the root cause # of these messages and check whether prepared statements are used correctly - # i.e. use bind markers for variable parts. # # Do only change the default value, if you really have more prepared statements than # fit in the cache. In most cases it is not necessary to change this value. # Constantly re-preparing statements is a performance penalty. # # Default value ("auto") is 1/256th of the heap or 10MB, whichever is greater prepared_statements_cache_size_mb: # Maximum size of the Thrift prepared statement cache # # If you do not use Thrift at all, it is safe to leave this value at "auto". # # See description of 'prepared_statements_cache_size_mb' above for more information. # # Default value ("auto") is 1/256th of the heap or 10MB, whichever is greater thrift_prepared_statements_cache_size_mb: # Maximum size of the key cache in memory. # # Each key cache hit saves 1 seek and each row cache hit saves 2 seeks at the # minimum, sometimes more. The key cache is fairly tiny for the amount of # time it saves, so it's worthwhile to use it at large numbers. # The row cache saves even more time, but must contain the entire row, # so it is extremely space-intensive. It's best to only use the # row cache if you have hot rows or static rows. # # NOTE: if you reduce the size, you may not get you hottest keys loaded on startup. # # Default value is empty to make it "auto" (min(5% of Heap (in MB), 100MB)). Set to 0 to disable key cache. key_cache_size_in_mb: # Duration in seconds after which Cassandra should # save the key cache. Caches are saved to saved_caches_directory as # specified in this configuration file. # # Saved caches greatly improve cold-start speeds, and is relatively cheap in # terms of I/O for the key cache. Row cache saving is much more expensive and # has limited use. # # Default is 14400 or 4 hours. key_cache_save_period: 14400 # Number of keys from the key cache to save # Disabled by default, meaning all keys are going to be saved # key_cache_keys_to_save: 100 # Row cache implementation class name. Available implementations: # # org.apache.cassandra.cache.OHCProvider # Fully off-heap row cache implementation (default). # # org.apache.cassandra.cache.SerializingCacheProvider # This is the row cache implementation available # in previous releases of Cassandra. # row_cache_class_name: org.apache.cassandra.cache.OHCProvider # Maximum size of the row cache in memory. # Please note that OHC cache implementation requires some additional off-heap memory to manage # the map structures and some in-flight memory during operations before/after cache entries can be # accounted against the cache capacity. This overhead is usually small compared to the whole capacity. # Do not specify more memory that the system can afford in the worst usual situation and leave some # headroom for OS block level cache. Do never allow your system to swap. # # Default value is 0, to disable row caching. row_cache_size_in_mb: 0 # Duration in seconds after which Cassandra should save the row cache. # Caches are saved to saved_caches_directory as specified in this configuration file. # # Saved caches greatly improve cold-start speeds, and is relatively cheap in # terms of I/O for the key cache. Row cache saving is much more expensive and # has limited use. # # Default is 0 to disable saving the row cache. row_cache_save_period: 0 # Number of keys from the row cache to save. # Specify 0 (which is the default), meaning all keys are going to be saved # row_cache_keys_to_save: 100 # Maximum size of the counter cache in memory. # # Counter cache helps to reduce counter locks' contention for hot counter cells. # In case of RF = 1 a counter cache hit will cause Cassandra to skip the read before # write entirely. With RF > 1 a counter cache hit will still help to reduce the duration # of the lock hold, helping with hot counter cell updates, but will not allow skipping # the read entirely. Only the local (clock, count) tuple of a counter cell is kept # in memory, not the whole counter, so it's relatively cheap. # # NOTE: if you reduce the size, you may not get you hottest keys loaded on startup. # # Default value is empty to make it "auto" (min(2.5% of Heap (in MB), 50MB)). Set to 0 to disable counter cache. # NOTE: if you perform counter deletes and rely on low gcgs, you should disable the counter cache. counter_cache_size_in_mb: # Duration in seconds after which Cassandra should # save the counter cache (keys only). Caches are saved to saved_caches_directory as # specified in this configuration file. # # Default is 7200 or 2 hours. counter_cache_save_period: 7200 # Number of keys from the counter cache to save # Disabled by default, meaning all keys are going to be saved # counter_cache_keys_to_save: 100 # saved caches # If not set, the default directory is $CASSANDRA_HOME/data/saved_caches. saved_caches_directory: /var/lib/cassandra/saved_caches # commitlog_sync may be either "periodic" or "batch." # # When in batch mode, Cassandra won't ack writes until the commit log # has been fsynced to disk. It will wait # commitlog_sync_batch_window_in_ms milliseconds between fsyncs. # This window should be kept short because the writer threads will # be unable to do extra work while waiting. (You may need to increase # concurrent_writes for the same reason.) # # commitlog_sync: batch # commitlog_sync_batch_window_in_ms: 2 # # the other option is "periodic" where writes may be acked immediately # and the CommitLog is simply synced every commitlog_sync_period_in_ms # milliseconds. commitlog_sync: periodic commitlog_sync_period_in_ms: 10000 # The size of the individual commitlog file segments. A commitlog # segment may be archived, deleted, or recycled once all the data # in it (potentially from each columnfamily in the system) has been # flushed to sstables. # # The default size is 32, which is almost always fine, but if you are # archiving commitlog segments (see commitlog_archiving.properties), # then you probably want a finer granularity of archiving; 8 or 16 MB # is reasonable. # Max mutation size is also configurable via max_mutation_size_in_kb setting in # cassandra.yaml. The default is half the size commitlog_segment_size_in_mb * 1024. # This should be positive and less than 2048. # # NOTE: If max_mutation_size_in_kb is set explicitly then commitlog_segment_size_in_mb must # be set to at least twice the size of max_mutation_size_in_kb / 1024 # commitlog_segment_size_in_mb: 32 # Compression to apply to the commit log. If omitted, the commit log # will be written uncompressed. LZ4, Snappy, and Deflate compressors # are supported. # commitlog_compression: # - class_name: LZ4Compressor # parameters: # - # any class that implements the SeedProvider interface and has a # constructor that takes a Map of parameters will do. seed_provider: # Addresses of hosts that are deemed contact points. # Cassandra nodes use this list of hosts to find each other and learn # the topology of the ring. You must change this if you are running # multiple nodes! - class_name: org.apache.cassandra.locator.SimpleSeedProvider parameters: # seeds is actually a comma-delimited list of addresses. # Ex: ",," - seeds: "172.17.0.2" # For workloads with more data than can fit in memory, Cassandra's # bottleneck will be reads that need to fetch data from # disk. "concurrent_reads" should be set to (16 * number_of_drives) in # order to allow the operations to enqueue low enough in the stack # that the OS and drives can reorder them. Same applies to # "concurrent_counter_writes", since counter writes read the current # values before incrementing and writing them back. # # On the other hand, since writes are almost never IO bound, the ideal # number of "concurrent_writes" is dependent on the number of cores in # your system; (8 * number_of_cores) is a good rule of thumb. concurrent_reads: 32 concurrent_writes: 32 concurrent_counter_writes: 32 # For materialized view writes, as there is a read involved, so this should # be limited by the less of concurrent reads or concurrent writes. concurrent_materialized_view_writes: 32 # Maximum memory to use for sstable chunk cache and buffer pooling. # 32MB of this are reserved for pooling buffers, the rest is used as a # cache that holds uncompressed sstable chunks. # Defaults to the smaller of 1/4 of heap or 512MB. This pool is allocated off-heap, # so is in addition to the memory allocated for heap. The cache also has on-heap # overhead which is roughly 128 bytes per chunk (i.e. 0.2% of the reserved size # if the default 64k chunk size is used). # Memory is only allocated when needed. # file_cache_size_in_mb: 512 # Flag indicating whether to allocate on or off heap when the sstable buffer # pool is exhausted, that is when it has exceeded the maximum memory # file_cache_size_in_mb, beyond which it will not cache buffers but allocate on request. # buffer_pool_use_heap_if_exhausted: true # The strategy for optimizing disk read # Possible values are: # ssd (for solid state disks, the default) # spinning (for spinning disks) # disk_optimization_strategy: ssd # Total permitted memory to use for memtables. Cassandra will stop # accepting writes when the limit is exceeded until a flush completes, # and will trigger a flush based on memtable_cleanup_threshold # If omitted, Cassandra will set both to 1/4 the size of the heap. # memtable_heap_space_in_mb: 2048 # memtable_offheap_space_in_mb: 2048 # memtable_cleanup_threshold is deprecated. The default calculation # is the only reasonable choice. See the comments on memtable_flush_writers # for more information. # # Ratio of occupied non-flushing memtable size to total permitted size # that will trigger a flush of the largest memtable. Larger mct will # mean larger flushes and hence less compaction, but also less concurrent # flush activity which can make it difficult to keep your disks fed # under heavy write load. # # memtable_cleanup_threshold defaults to 1 / (memtable_flush_writers + 1) # memtable_cleanup_threshold: 0.11 # Specify the way Cassandra allocates and manages memtable memory. # Options are: # # heap_buffers # on heap nio buffers # # offheap_buffers # off heap (direct) nio buffers # # offheap_objects # off heap objects memtable_allocation_type: heap_buffers # Total space to use for commit logs on disk. # # If space gets above this value, Cassandra will flush every dirty CF # in the oldest segment and remove it. So a small total commitlog space # will tend to cause more flush activity on less-active columnfamilies. # # The default value is the smaller of 8192, and 1/4 of the total space # of the commitlog volume. # # commitlog_total_space_in_mb: 8192 # This sets the number of memtable flush writer threads per disk # as well as the total number of memtables that can be flushed concurrently. # These are generally a combination of compute and IO bound. # # Memtable flushing is more CPU efficient than memtable ingest and a single thread # can keep up with the ingest rate of a whole server on a single fast disk # until it temporarily becomes IO bound under contention typically with compaction. # At that point you need multiple flush threads. At some point in the future # it may become CPU bound all the time. # # You can tell if flushing is falling behind using the MemtablePool.BlockedOnAllocation # metric which should be 0, but will be non-zero if threads are blocked waiting on flushing # to free memory. # # memtable_flush_writers defaults to two for a single data directory. # This means that two memtables can be flushed concurrently to the single data directory. # If you have multiple data directories the default is one memtable flushing at a time # but the flush will use a thread per data directory so you will get two or more writers. # # Two is generally enough to flush on a fast disk [array] mounted as a single data directory. # Adding more flush writers will result in smaller more frequent flushes that introduce more # compaction overhead. # # There is a direct tradeoff between number of memtables that can be flushed concurrently # and flush size and frequency. More is not better you just need enough flush writers # to never stall waiting for flushing to free memory. # #memtable_flush_writers: 2 # Total space to use for change-data-capture logs on disk. # # If space gets above this value, Cassandra will throw WriteTimeoutException # on Mutations including tables with CDC enabled. A CDCCompactor is responsible # for parsing the raw CDC logs and deleting them when parsing is completed. # # The default value is the min of 4096 mb and 1/8th of the total space # of the drive where cdc_raw_directory resides. # cdc_total_space_in_mb: 4096 # When we hit our cdc_raw limit and the CDCCompactor is either running behind # or experiencing backpressure, we check at the following interval to see if any # new space for cdc-tracked tables has been made available. Default to 250ms # cdc_free_space_check_interval_ms: 250 # A fixed memory pool size in MB for SSTable index summaries. If left # empty, this will default to 5% of the heap size. If the memory usage of # all index summaries exceeds this limit, SSTables with low read rates will # shrink their index summaries in order to meet this limit. However, this # is a best-effort process. In extreme conditions Cassandra may need to use # more than this amount of memory. index_summary_capacity_in_mb: # How frequently index summaries should be resampled. This is done # periodically to redistribute memory from the fixed-size pool to sstables # proportional their recent read rates. Setting to -1 will disable this # process, leaving existing index summaries at their current sampling level. index_summary_resize_interval_in_minutes: 60 # Whether to, when doing sequential writing, fsync() at intervals in # order to force the operating system to flush the dirty # buffers. Enable this to avoid sudden dirty buffer flushing from # impacting read latencies. Almost always a good idea on SSDs; not # necessarily on platters. trickle_fsync: false trickle_fsync_interval_in_kb: 10240 # TCP port, for commands and data # For security reasons, you should not expose this port to the internet. Firewall it if needed. storage_port: 7000 # SSL port, for encrypted communication. Unused unless enabled in # encryption_options # For security reasons, you should not expose this port to the internet. Firewall it if needed. ssl_storage_port: 7001 # Address or interface to bind to and tell other Cassandra nodes to connect to. # You _must_ change this if you want multiple nodes to be able to communicate! # # Set listen_address OR listen_interface, not both. # # Leaving it blank leaves it up to InetAddress.getLocalHost(). This # will always do the Right Thing _if_ the node is properly configured # (hostname, name resolution, etc), and the Right Thing is to use the # address associated with the hostname (it might not be). # # Setting listen_address to 0.0.0.0 is always wrong. # listen_address: 172.17.0.2 # Set listen_address OR listen_interface, not both. Interfaces must correspond # to a single address, IP aliasing is not supported. # listen_interface: eth0 # If you choose to specify the interface by name and the interface has an ipv4 and an ipv6 address # you can specify which should be chosen using listen_interface_prefer_ipv6. If false the first ipv4 # address will be used. If true the first ipv6 address will be used. Defaults to false preferring # ipv4. If there is only one address it will be selected regardless of ipv4/ipv6. # listen_interface_prefer_ipv6: false # Address to broadcast to other Cassandra nodes # Leaving this blank will set it to the same value as listen_address broadcast_address: 172.17.0.2 # When using multiple physical network interfaces, set this # to true to listen on broadcast_address in addition to # the listen_address, allowing nodes to communicate in both # interfaces. # Ignore this property if the network configuration automatically # routes between the public and private networks such as EC2. # listen_on_broadcast_address: false # Internode authentication backend, implementing IInternodeAuthenticator; # used to allow/disallow connections from peer nodes. # internode_authenticator: org.apache.cassandra.auth.AllowAllInternodeAuthenticator # Whether to start the native transport server. # Please note that the address on which the native transport is bound is the # same as the rpc_address. The port however is different and specified below. start_native_transport: true # port for the CQL native transport to listen for clients on # For security reasons, you should not expose this port to the internet. Firewall it if needed. native_transport_port: 9042 # Enabling native transport encryption in client_encryption_options allows you to either use # encryption for the standard port or to use a dedicated, additional port along with the unencrypted # standard native_transport_port. # Enabling client encryption and keeping native_transport_port_ssl disabled will use encryption # for native_transport_port. Setting native_transport_port_ssl to a different value # from native_transport_port will use encryption for native_transport_port_ssl while # keeping native_transport_port unencrypted. # native_transport_port_ssl: 9142 # The maximum threads for handling requests when the native transport is used. # This is similar to rpc_max_threads though the default differs slightly (and # there is no native_transport_min_threads, idle threads will always be stopped # after 30 seconds). # native_transport_max_threads: 128 # # The maximum size of allowed frame. Frame (requests) larger than this will # be rejected as invalid. The default is 256MB. If you're changing this parameter, # you may want to adjust max_value_size_in_mb accordingly. This should be positive and less than 2048. # native_transport_max_frame_size_in_mb: 256 # The maximum number of concurrent client connections. # The default is -1, which means unlimited. # native_transport_max_concurrent_connections: -1 # The maximum number of concurrent client connections per source ip. # The default is -1, which means unlimited. # native_transport_max_concurrent_connections_per_ip: -1 # Whether to start the thrift rpc server. start_rpc: false # The address or interface to bind the Thrift RPC service and native transport # server to. # # Set rpc_address OR rpc_interface, not both. # # Leaving rpc_address blank has the same effect as on listen_address # (i.e. it will be based on the configured hostname of the node). # # Note that unlike listen_address, you can specify 0.0.0.0, but you must also # set broadcast_rpc_address to a value other than 0.0.0.0. # # For security reasons, you should not expose this port to the internet. Firewall it if needed. rpc_address: 0.0.0.0 # Set rpc_address OR rpc_interface, not both. Interfaces must correspond # to a single address, IP aliasing is not supported. # rpc_interface: eth1 # If you choose to specify the interface by name and the interface has an ipv4 and an ipv6 address # you can specify which should be chosen using rpc_interface_prefer_ipv6. If false the first ipv4 # address will be used. If true the first ipv6 address will be used. Defaults to false preferring # ipv4. If there is only one address it will be selected regardless of ipv4/ipv6. # rpc_interface_prefer_ipv6: false # port for Thrift to listen for clients on rpc_port: 9160 # RPC address to broadcast to drivers and other Cassandra nodes. This cannot # be set to 0.0.0.0. If left blank, this will be set to the value of # rpc_address. If rpc_address is set to 0.0.0.0, broadcast_rpc_address must # be set. broadcast_rpc_address: 172.17.0.2 # enable or disable keepalive on rpc/native connections rpc_keepalive: true # Cassandra provides two out-of-the-box options for the RPC Server: # # sync # One thread per thrift connection. For a very large number of clients, memory # will be your limiting factor. On a 64 bit JVM, 180KB is the minimum stack size # per thread, and that will correspond to your use of virtual memory (but physical memory # may be limited depending on use of stack space). # # hsha # Stands for "half synchronous, half asynchronous." All thrift clients are handled # asynchronously using a small number of threads that does not vary with the amount # of thrift clients (and thus scales well to many clients). The rpc requests are still # synchronous (one thread per active request). If hsha is selected then it is essential # that rpc_max_threads is changed from the default value of unlimited. # # The default is sync because on Windows hsha is about 30% slower. On Linux, # sync/hsha performance is about the same, with hsha of course using less memory. # # Alternatively, can provide your own RPC server by providing the fully-qualified class name # of an o.a.c.t.TServerFactory that can create an instance of it. rpc_server_type: sync # Uncomment rpc_min|max_thread to set request pool size limits. # # Regardless of your choice of RPC server (see above), the number of maximum requests in the # RPC thread pool dictates how many concurrent requests are possible (but if you are using the sync # RPC server, it also dictates the number of clients that can be connected at all). # # The default is unlimited and thus provides no protection against clients overwhelming the server. You are # encouraged to set a maximum that makes sense for you in production, but do keep in mind that # rpc_max_threads represents the maximum number of client requests this server may execute concurrently. # # rpc_min_threads: 16 # rpc_max_threads: 2048 # uncomment to set socket buffer sizes on rpc connections # rpc_send_buff_size_in_bytes: # rpc_recv_buff_size_in_bytes: # Uncomment to set socket buffer size for internode communication # Note that when setting this, the buffer size is limited by net.core.wmem_max # and when not setting it it is defined by net.ipv4.tcp_wmem # See also: # /proc/sys/net/core/wmem_max # /proc/sys/net/core/rmem_max # /proc/sys/net/ipv4/tcp_wmem # /proc/sys/net/ipv4/tcp_wmem # and 'man tcp' # internode_send_buff_size_in_bytes: # Uncomment to set socket buffer size for internode communication # Note that when setting this, the buffer size is limited by net.core.wmem_max # and when not setting it it is defined by net.ipv4.tcp_wmem # internode_recv_buff_size_in_bytes: # Frame size for thrift (maximum message length). thrift_framed_transport_size_in_mb: 15 # Set to true to have Cassandra create a hard link to each sstable # flushed or streamed locally in a backups/ subdirectory of the # keyspace data. Removing these links is the operator's # responsibility. incremental_backups: false # Whether or not to take a snapshot before each compaction. Be # careful using this option, since Cassandra won't clean up the # snapshots for you. Mostly useful if you're paranoid when there # is a data format change. snapshot_before_compaction: false # Whether or not a snapshot is taken of the data before keyspace truncation # or dropping of column families. The STRONGLY advised default of true # should be used to provide data safety. If you set this flag to false, you will # lose data on truncation or drop. auto_snapshot: true # Granularity of the collation index of rows within a partition. # Increase if your rows are large, or if you have a very large # number of rows per partition. The competing goals are these: # # - a smaller granularity means more index entries are generated # and looking up rows within the partition by collation column # is faster # - but, Cassandra will keep the collation index in memory for hot # rows (as part of the key cache), so a larger granularity means # you can cache more hot rows column_index_size_in_kb: 64 # Per sstable indexed key cache entries (the collation index in memory # mentioned above) exceeding this size will not be held on heap. # This means that only partition information is held on heap and the # index entries are read from disk. # # Note that this size refers to the size of the # serialized index information and not the size of the partition. column_index_cache_size_in_kb: 2 # Number of simultaneous compactions to allow, NOT including # validation "compactions" for anti-entropy repair. Simultaneous # compactions can help preserve read performance in a mixed read/write # workload, by mitigating the tendency of small sstables to accumulate # during a single long running compactions. The default is usually # fine and if you experience problems with compaction running too # slowly or too fast, you should look at # compaction_throughput_mb_per_sec first. # # concurrent_compactors defaults to the smaller of (number of disks, # number of cores), with a minimum of 2 and a maximum of 8. # # If your data directories are backed by SSD, you should increase this # to the number of cores. #concurrent_compactors: 1 # Throttles compaction to the given total throughput across the entire # system. The faster you insert data, the faster you need to compact in # order to keep the sstable count down, but in general, setting this to # 16 to 32 times the rate you are inserting data is more than sufficient. # Setting this to 0 disables throttling. Note that this account for all types # of compaction, including validation compaction. compaction_throughput_mb_per_sec: 16 # When compacting, the replacement sstable(s) can be opened before they # are completely written, and used in place of the prior sstables for # any range that has been written. This helps to smoothly transfer reads # between the sstables, reducing page cache churn and keeping hot rows hot sstable_preemptive_open_interval_in_mb: 50 # Throttles all outbound streaming file transfers on this node to the # given total throughput in Mbps. This is necessary because Cassandra does # mostly sequential IO when streaming data during bootstrap or repair, which # can lead to saturating the network connection and degrading rpc performance. # When unset, the default is 200 Mbps or 25 MB/s. # stream_throughput_outbound_megabits_per_sec: 200 # Throttles all streaming file transfer between the datacenters, # this setting allows users to throttle inter dc stream throughput in addition # to throttling all network stream traffic as configured with # stream_throughput_outbound_megabits_per_sec # When unset, the default is 200 Mbps or 25 MB/s # inter_dc_stream_throughput_outbound_megabits_per_sec: 200 # How long the coordinator should wait for read operations to complete read_request_timeout_in_ms: 5000 # How long the coordinator should wait for seq or index scans to complete range_request_timeout_in_ms: 10000 # How long the coordinator should wait for writes to complete write_request_timeout_in_ms: 2000 # How long the coordinator should wait for counter writes to complete counter_write_request_timeout_in_ms: 5000 # How long a coordinator should continue to retry a CAS operation # that contends with other proposals for the same row cas_contention_timeout_in_ms: 1000 # How long the coordinator should wait for truncates to complete # (This can be much longer, because unless auto_snapshot is disabled # we need to flush first so we can snapshot before removing the data.) truncate_request_timeout_in_ms: 60000 # The default timeout for other, miscellaneous operations request_timeout_in_ms: 10000 # How long before a node logs slow queries. Select queries that take longer than # this timeout to execute, will generate an aggregated log message, so that slow queries # can be identified. Set this value to zero to disable slow query logging. slow_query_log_timeout_in_ms: 500 # Enable operation timeout information exchange between nodes to accurately # measure request timeouts. If disabled, replicas will assume that requests # were forwarded to them instantly by the coordinator, which means that # under overload conditions we will waste that much extra time processing # already-timed-out requests. # # Warning: before enabling this property make sure to ntp is installed # and the times are synchronized between the nodes. cross_node_timeout: false # Set keep-alive period for streaming # This node will send a keep-alive message periodically with this period. # If the node does not receive a keep-alive message from the peer for # 2 keep-alive cycles the stream session times out and fail # Default value is 300s (5 minutes), which means stalled stream # times out in 10 minutes by default # streaming_keep_alive_period_in_secs: 300 # phi value that must be reached for a host to be marked down. # most users should never need to adjust this. # phi_convict_threshold: 8 # endpoint_snitch -- Set this to a class that implements # IEndpointSnitch. The snitch has two functions: # # - it teaches Cassandra enough about your network topology to route # requests efficiently # - it allows Cassandra to spread replicas around your cluster to avoid # correlated failures. It does this by grouping machines into # "datacenters" and "racks." Cassandra will do its best not to have # more than one replica on the same "rack" (which may not actually # be a physical location) # # CASSANDRA WILL NOT ALLOW YOU TO SWITCH TO AN INCOMPATIBLE SNITCH # ONCE DATA IS INSERTED INTO THE CLUSTER. This would cause data loss. # This means that if you start with the default SimpleSnitch, which # locates every node on "rack1" in "datacenter1", your only options # if you need to add another datacenter are GossipingPropertyFileSnitch # (and the older PFS). From there, if you want to migrate to an # incompatible snitch like Ec2Snitch you can do it by adding new nodes # under Ec2Snitch (which will locate them in a new "datacenter") and # decommissioning the old ones. # # Out of the box, Cassandra provides: # # SimpleSnitch: # Treats Strategy order as proximity. This can improve cache # locality when disabling read repair. Only appropriate for # single-datacenter deployments. # # GossipingPropertyFileSnitch # This should be your go-to snitch for production use. The rack # and datacenter for the local node are defined in # cassandra-rackdc.properties and propagated to other nodes via # gossip. If cassandra-topology.properties exists, it is used as a # fallback, allowing migration from the PropertyFileSnitch. # # PropertyFileSnitch: # Proximity is determined by rack and data center, which are # explicitly configured in cassandra-topology.properties. # # Ec2Snitch: # Appropriate for EC2 deployments in a single Region. Loads Region # and Availability Zone information from the EC2 API. The Region is # treated as the datacenter, and the Availability Zone as the rack. # Only private IPs are used, so this will not work across multiple # Regions. # # Ec2MultiRegionSnitch: # Uses public IPs as broadcast_address to allow cross-region # connectivity. (Thus, you should set seed addresses to the public # IP as well.) You will need to open the storage_port or # ssl_storage_port on the public IP firewall. (For intra-Region # traffic, Cassandra will switch to the private IP after # establishing a connection.) # # RackInferringSnitch: # Proximity is determined by rack and data center, which are # assumed to correspond to the 3rd and 2nd octet of each node's IP # address, respectively. Unless this happens to match your # deployment conventions, this is best used as an example of # writing a custom Snitch class and is provided in that spirit. # # You can use a custom Snitch by setting this to the full class name # of the snitch, which will be assumed to be on your classpath. endpoint_snitch: SimpleSnitch # controls how often to perform the more expensive part of host score # calculation dynamic_snitch_update_interval_in_ms: 100 # controls how often to reset all host scores, allowing a bad host to # possibly recover dynamic_snitch_reset_interval_in_ms: 600000 # if set greater than zero and read_repair_chance is < 1.0, this will allow # 'pinning' of replicas to hosts in order to increase cache capacity. # The badness threshold will control how much worse the pinned host has to be # before the dynamic snitch will prefer other replicas over it. This is # expressed as a double which represents a percentage. Thus, a value of # 0.2 means Cassandra would continue to prefer the static snitch values # until the pinned host was 20% worse than the fastest. dynamic_snitch_badness_threshold: 0.1 # request_scheduler -- Set this to a class that implements # RequestScheduler, which will schedule incoming client requests # according to the specific policy. This is useful for multi-tenancy # with a single Cassandra cluster. # NOTE: This is specifically for requests from the client and does # not affect inter node communication. # org.apache.cassandra.scheduler.NoScheduler - No scheduling takes place # org.apache.cassandra.scheduler.RoundRobinScheduler - Round robin of # client requests to a node with a separate queue for each # request_scheduler_id. The scheduler is further customized by # request_scheduler_options as described below. request_scheduler: org.apache.cassandra.scheduler.NoScheduler # Scheduler Options vary based on the type of scheduler # # NoScheduler # Has no options # # RoundRobin # throttle_limit # The throttle_limit is the number of in-flight # requests per client. Requests beyond # that limit are queued up until # running requests can complete. # The value of 80 here is twice the number of # concurrent_reads + concurrent_writes. # default_weight # default_weight is optional and allows for # overriding the default which is 1. # weights # Weights are optional and will default to 1 or the # overridden default_weight. The weight translates into how # many requests are handled during each turn of the # RoundRobin, based on the scheduler id. # # request_scheduler_options: # throttle_limit: 80 # default_weight: 5 # weights: # Keyspace1: 1 # Keyspace2: 5 # request_scheduler_id -- An identifier based on which to perform # the request scheduling. Currently the only valid option is keyspace. # request_scheduler_id: keyspace # Enable or disable inter-node encryption # JVM defaults for supported SSL socket protocols and cipher suites can # be replaced using custom encryption options. This is not recommended # unless you have policies in place that dictate certain settings, or # need to disable vulnerable ciphers or protocols in case the JVM cannot # be updated. # FIPS compliant settings can be configured at JVM level and should not # involve changing encryption settings here: # https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/FIPS.html # *NOTE* No custom encryption options are enabled at the moment # The available internode options are : all, none, dc, rack # # If set to dc cassandra will encrypt the traffic between the DCs # If set to rack cassandra will encrypt the traffic between the racks # # The passwords used in these options must match the passwords used when generating # the keystore and truststore. For instructions on generating these files, see: # http://download.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html#CreateKeystore # server_encryption_options: internode_encryption: none keystore: conf/.keystore keystore_password: cassandra truststore: conf/.truststore truststore_password: cassandra # More advanced defaults below: # protocol: TLS # algorithm: SunX509 # store_type: JKS # cipher_suites: [TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA] # require_client_auth: false # require_endpoint_verification: false # enable or disable client/server encryption. client_encryption_options: enabled: false # If enabled and optional is set to true encrypted and unencrypted connections are handled. optional: false keystore: conf/.keystore keystore_password: cassandra # require_client_auth: false # Set trustore and truststore_password if require_client_auth is true # truststore: conf/.truststore # truststore_password: cassandra # More advanced defaults below: # protocol: TLS # algorithm: SunX509 # store_type: JKS # cipher_suites: [TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA] # internode_compression controls whether traffic between nodes is # compressed. # Can be: # # all # all traffic is compressed # # dc # traffic between different datacenters is compressed # # none # nothing is compressed. internode_compression: dc # Enable or disable tcp_nodelay for inter-dc communication. # Disabling it will result in larger (but fewer) network packets being sent, # reducing overhead from the TCP protocol itself, at the cost of increasing # latency if you block for cross-datacenter responses. inter_dc_tcp_nodelay: false # TTL for different trace types used during logging of the repair process. tracetype_query_ttl: 86400 tracetype_repair_ttl: 604800 # By default, Cassandra logs GC Pauses greater than 200 ms at INFO level # This threshold can be adjusted to minimize logging if necessary # gc_log_threshold_in_ms: 200 # If unset, all GC Pauses greater than gc_log_threshold_in_ms will log at # INFO level # UDFs (user defined functions) are disabled by default. # As of Cassandra 3.0 there is a sandbox in place that should prevent execution of evil code. enable_user_defined_functions: false # Enables scripted UDFs (JavaScript UDFs). # Java UDFs are always enabled, if enable_user_defined_functions is true. # Enable this option to be able to use UDFs with "language javascript" or any custom JSR-223 provider. # This option has no effect, if enable_user_defined_functions is false. enable_scripted_user_defined_functions: false # The default Windows kernel timer and scheduling resolution is 15.6ms for power conservation. # Lowering this value on Windows can provide much tighter latency and better throughput, however # some virtualized environments may see a negative performance impact from changing this setting # below their system default. The sysinternals 'clockres' tool can confirm your system's default # setting. windows_timer_interval: 1 # Enables encrypting data at-rest (on disk). Different key providers can be plugged in, but the default reads from # a JCE-style keystore. A single keystore can hold multiple keys, but the one referenced by # the "key_alias" is the only key that will be used for encrypt operations; previously used keys # can still (and should!) be in the keystore and will be used on decrypt operations # (to handle the case of key rotation). # # It is strongly recommended to download and install Java Cryptography Extension (JCE) # Unlimited Strength Jurisdiction Policy Files for your version of the JDK. # (current link: http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html) # # Currently, only the following file types are supported for transparent data encryption, although # more are coming in future cassandra releases: commitlog, hints transparent_data_encryption_options: enabled: false chunk_length_kb: 64 cipher: AES/CBC/PKCS5Padding key_alias: testing:1 # CBC IV length for AES needs to be 16 bytes (which is also the default size) # iv_length: 16 key_provider: - class_name: org.apache.cassandra.security.JKSKeyProvider parameters: - keystore: conf/.keystore keystore_password: cassandra store_type: JCEKS key_password: cassandra ##################### # SAFETY THRESHOLDS # ##################### # When executing a scan, within or across a partition, we need to keep the # tombstones seen in memory so we can return them to the coordinator, which # will use them to make sure other replicas also know about the deleted rows. # With workloads that generate a lot of tombstones, this can cause performance # problems and even exhaust the server heap. # (http://www.datastax.com/dev/blog/cassandra-anti-patterns-queues-and-queue-like-datasets) # Adjust the thresholds here if you understand the dangers and want to # scan more tombstones anyway. These thresholds may also be adjusted at runtime # using the StorageService mbean. tombstone_warn_threshold: 1000 tombstone_failure_threshold: 100000 # Log WARN on any multiple-partition batch size exceeding this value. 5kb per batch by default. # Caution should be taken on increasing the size of this threshold as it can lead to node instability. batch_size_warn_threshold_in_kb: 5 # Fail any multiple-partition batch exceeding this value. 50kb (10x warn threshold) by default. batch_size_fail_threshold_in_kb: 50 # Log WARN on any batches not of type LOGGED than span across more partitions than this limit unlogged_batch_across_partitions_warn_threshold: 10 # Log a warning when compacting partitions larger than this value compaction_large_partition_warning_threshold_mb: 100 # GC Pauses greater than gc_warn_threshold_in_ms will be logged at WARN level # Adjust the threshold based on your application throughput requirement # By default, Cassandra logs GC Pauses greater than 200 ms at INFO level gc_warn_threshold_in_ms: 1000 # Maximum size of any value in SSTables. Safety measure to detect SSTable corruption # early. Any value size larger than this threshold will result into marking an SSTable # as corrupted. This should be positive and less than 2048. # max_value_size_in_mb: 256 # Back-pressure settings # # If enabled, the coordinator will apply the back-pressure strategy specified below to each mutation # sent to replicas, with the aim of reducing pressure on overloaded replicas. back_pressure_enabled: false # The back-pressure strategy applied. # The default implementation, RateBasedBackPressure, takes three arguments: # high ratio, factor, and flow type, and uses the ratio between incoming mutation responses and outgoing mutation requests. # If below high ratio, outgoing mutations are rate limited according to the incoming rate decreased by the given factor; # if above high ratio, the rate limiting is increased by the given factor; # such factor is usually best configured between 1 and 10, use larger values for a faster recovery # at the expense of potentially more dropped mutations; # the rate limiting is applied according to the flow type: if FAST, it's rate limited at the speed of the fastest replica, # if SLOW at the speed of the slowest one. # New strategies can be added. Implementors need to implement org.apache.cassandra.net.BackpressureStrategy and # provide a public constructor accepting a Map. back_pressure_strategy: - class_name: org.apache.cassandra.net.RateBasedBackPressure parameters: - high_ratio: 0.90 factor: 5 flow: FAST # Coalescing Strategies # # Coalescing multiples messages turns out to significantly boost message processing throughput (think doubling or more). # On bare metal, the floor for packet processing throughput is high enough that many applications won't notice, but in # virtualized environments, the point at which an application can be bound by network packet processing can be # surprisingly low compared to the throughput of task processing that is possible inside a VM. It's not that bare metal # doesn't benefit from coalescing messages, it's that the number of packets a bare metal network interface can process # is sufficient for many applications such that no load starvation is experienced even without coalescing. # There are other benefits to coalescing network messages that are harder to isolate with a simple metric like messages # per second. By coalescing multiple tasks together, a network thread can process multiple messages for the cost of one # trip to read from a socket, and all the task submission work can be done at the same time reducing context switching # and increasing cache friendliness of network message processing. # See CASSANDRA-8692 for details. # Strategy to use for coalescing messages in OutboundTcpConnection. # Can be fixed, movingaverage, timehorizon, disabled (default). # You can also specify a subclass of CoalescingStrategies.CoalescingStrategy by name. # otc_coalescing_strategy: DISABLED # How many microseconds to wait for coalescing. For fixed strategy this is the amount of time after the first # message is received before it will be sent with any accompanying messages. For moving average this is the # maximum amount of time that will be waited as well as the interval at which messages must arrive on average # for coalescing to be enabled. # otc_coalescing_window_us: 200 # Do not try to coalesce messages if we already got that many messages. This should be more than 2 and less than 128. # otc_coalescing_enough_coalesced_messages: 8 # How many milliseconds to wait between two expiration runs on the backlog (queue) of the OutboundTcpConnection. # Expiration is done if messages are piling up in the backlog. Droppable messages are expired to free the memory # taken by expired messages. The interval should be between 0 and 1000, and in most installations the default value # will be appropriate. A smaller value could potentially expire messages slightly sooner at the expense of more CPU # time and queue contention while iterating the backlog of messages. # An interval of 0 disables any wait time, which is the behavior of former Cassandra versions. # # otc_backlog_expiration_interval_ms: 200 ================================================ FILE: modules/cassandra/src/test/resources/cassandra-ssl-configuration/cassandra.yaml ================================================ # Cassandra storage config YAML # NOTE: # See http://wiki.apache.org/cassandra/StorageConfiguration for # full explanations of configuration directives # /NOTE # The name of the cluster. This is mainly used to prevent machines in # one logical cluster from joining another. cluster_name: 'Test Cluster Integration Test' # This defines the number of tokens randomly assigned to this node on the ring # The more tokens, relative to other nodes, the larger the proportion of data # that this node will store. You probably want all nodes to have the same number # of tokens assuming they have equal hardware capability. # # If you leave this unspecified, Cassandra will use the default of 1 token for legacy compatibility, # and will use the initial_token as described below. # # Specifying initial_token will override this setting on the node's initial start, # on subsequent starts, this setting will apply even if initial token is set. # # If you already have a cluster with 1 token per node, and wish to migrate to # multiple tokens per node, see http://wiki.apache.org/cassandra/Operations num_tokens: 256 # Triggers automatic allocation of num_tokens tokens for this node. The allocation # algorithm attempts to choose tokens in a way that optimizes replicated load over # the nodes in the datacenter for the replication strategy used by the specified # keyspace. # # The load assigned to each node will be close to proportional to its number of # vnodes. # # Only supported with the Murmur3Partitioner. # allocate_tokens_for_keyspace: KEYSPACE # initial_token allows you to specify tokens manually. While you can use it with # vnodes (num_tokens > 1, above) -- in which case you should provide a # comma-separated list -- it's primarily used when adding nodes to legacy clusters # that do not have vnodes enabled. # initial_token: # See http://wiki.apache.org/cassandra/HintedHandoff # May either be "true" or "false" to enable globally hinted_handoff_enabled: true # When hinted_handoff_enabled is true, a black list of data centers that will not # perform hinted handoff # hinted_handoff_disabled_datacenters: # - DC1 # - DC2 # this defines the maximum amount of time a dead host will have hints # generated. After it has been dead this long, new hints for it will not be # created until it has been seen alive and gone down again. max_hint_window_in_ms: 10800000 # 3 hours # Maximum throttle in KBs per second, per delivery thread. This will be # reduced proportionally to the number of nodes in the cluster. (If there # are two nodes in the cluster, each delivery thread will use the maximum # rate; if there are three, each will throttle to half of the maximum, # since we expect two nodes to be delivering hints simultaneously.) hinted_handoff_throttle_in_kb: 1024 # Number of threads with which to deliver hints; # Consider increasing this number when you have multi-dc deployments, since # cross-dc handoff tends to be slower max_hints_delivery_threads: 2 # Directory where Cassandra should store hints. # If not set, the default directory is $CASSANDRA_HOME/data/hints. # hints_directory: /var/lib/cassandra/hints # How often hints should be flushed from the internal buffers to disk. # Will *not* trigger fsync. hints_flush_period_in_ms: 10000 # Maximum size for a single hints file, in megabytes. max_hints_file_size_in_mb: 128 # Compression to apply to the hint files. If omitted, hints files # will be written uncompressed. LZ4, Snappy, and Deflate compressors # are supported. #hints_compression: # - class_name: LZ4Compressor # parameters: # - # Maximum throttle in KBs per second, total. This will be # reduced proportionally to the number of nodes in the cluster. batchlog_replay_throttle_in_kb: 1024 # Authentication backend, implementing IAuthenticator; used to identify users # Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthenticator, # PasswordAuthenticator}. # # - AllowAllAuthenticator performs no checks - set it to disable authentication. # - PasswordAuthenticator relies on username/password pairs to authenticate # users. It keeps usernames and hashed passwords in system_auth.roles table. # Please increase system_auth keyspace replication factor if you use this authenticator. # If using PasswordAuthenticator, CassandraRoleManager must also be used (see below) authenticator: AllowAllAuthenticator # Authorization backend, implementing IAuthorizer; used to limit access/provide permissions # Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthorizer, # CassandraAuthorizer}. # # - AllowAllAuthorizer allows any action to any user - set it to disable authorization. # - CassandraAuthorizer stores permissions in system_auth.role_permissions table. Please # increase system_auth keyspace replication factor if you use this authorizer. authorizer: AllowAllAuthorizer # Part of the Authentication & Authorization backend, implementing IRoleManager; used # to maintain grants and memberships between roles. # Out of the box, Cassandra provides org.apache.cassandra.auth.CassandraRoleManager, # which stores role information in the system_auth keyspace. Most functions of the # IRoleManager require an authenticated login, so unless the configured IAuthenticator # actually implements authentication, most of this functionality will be unavailable. # # - CassandraRoleManager stores role data in the system_auth keyspace. Please # increase system_auth keyspace replication factor if you use this role manager. role_manager: CassandraRoleManager # Validity period for roles cache (fetching granted roles can be an expensive # operation depending on the role manager, CassandraRoleManager is one example) # Granted roles are cached for authenticated sessions in AuthenticatedUser and # after the period specified here, become eligible for (async) reload. # Defaults to 2000, set to 0 to disable caching entirely. # Will be disabled automatically for AllowAllAuthenticator. roles_validity_in_ms: 2000 # Refresh interval for roles cache (if enabled). # After this interval, cache entries become eligible for refresh. Upon next # access, an async reload is scheduled and the old value returned until it # completes. If roles_validity_in_ms is non-zero, then this must be # also. # Defaults to the same value as roles_validity_in_ms. # roles_update_interval_in_ms: 2000 # Validity period for permissions cache (fetching permissions can be an # expensive operation depending on the authorizer, CassandraAuthorizer is # one example). Defaults to 2000, set to 0 to disable. # Will be disabled automatically for AllowAllAuthorizer. permissions_validity_in_ms: 2000 # Refresh interval for permissions cache (if enabled). # After this interval, cache entries become eligible for refresh. Upon next # access, an async reload is scheduled and the old value returned until it # completes. If permissions_validity_in_ms is non-zero, then this must be # also. # Defaults to the same value as permissions_validity_in_ms. # permissions_update_interval_in_ms: 2000 # Validity period for credentials cache. This cache is tightly coupled to # the provided PasswordAuthenticator implementation of IAuthenticator. If # another IAuthenticator implementation is configured, this cache will not # be automatically used and so the following settings will have no effect. # Please note, credentials are cached in their encrypted form, so while # activating this cache may reduce the number of queries made to the # underlying table, it may not bring a significant reduction in the # latency of individual authentication attempts. # Defaults to 2000, set to 0 to disable credentials caching. credentials_validity_in_ms: 2000 # Refresh interval for credentials cache (if enabled). # After this interval, cache entries become eligible for refresh. Upon next # access, an async reload is scheduled and the old value returned until it # completes. If credentials_validity_in_ms is non-zero, then this must be # also. # Defaults to the same value as credentials_validity_in_ms. # credentials_update_interval_in_ms: 2000 # The partitioner is responsible for distributing groups of rows (by # partition key) across nodes in the cluster. You should leave this # alone for new clusters. The partitioner can NOT be changed without # reloading all data, so when upgrading you should set this to the # same partitioner you were already using. # # Besides Murmur3Partitioner, partitioners included for backwards # compatibility include RandomPartitioner, ByteOrderedPartitioner, and # OrderPreservingPartitioner. # partitioner: org.apache.cassandra.dht.Murmur3Partitioner # Directories where Cassandra should store data on disk. Cassandra # will spread data evenly across them, subject to the granularity of # the configured compaction strategy. # If not set, the default directory is $CASSANDRA_HOME/data/data. data_file_directories: - /var/lib/cassandra/data # commit log. when running on magnetic HDD, this should be a # separate spindle than the data directories. # If not set, the default directory is $CASSANDRA_HOME/data/commitlog. commitlog_directory: /var/lib/cassandra/commitlog # Enable / disable CDC functionality on a per-node basis. This modifies the logic used # for write path allocation rejection (standard: never reject. cdc: reject Mutation # containing a CDC-enabled table if at space limit in cdc_raw_directory). cdc_enabled: false # CommitLogSegments are moved to this directory on flush if cdc_enabled: true and the # segment contains mutations for a CDC-enabled table. This should be placed on a # separate spindle than the data directories. If not set, the default directory is # $CASSANDRA_HOME/data/cdc_raw. # cdc_raw_directory: /var/lib/cassandra/cdc_raw # Policy for data disk failures: # # die # shut down gossip and client transports and kill the JVM for any fs errors or # single-sstable errors, so the node can be replaced. # # stop_paranoid # shut down gossip and client transports even for single-sstable errors, # kill the JVM for errors during startup. # # stop # shut down gossip and client transports, leaving the node effectively dead, but # can still be inspected via JMX, kill the JVM for errors during startup. # # best_effort # stop using the failed disk and respond to requests based on # remaining available sstables. This means you WILL see obsolete # data at CL.ONE! # # ignore # ignore fatal errors and let requests fail, as in pre-1.2 Cassandra disk_failure_policy: stop # Policy for commit disk failures: # # die # shut down gossip and Thrift and kill the JVM, so the node can be replaced. # # stop # shut down gossip and Thrift, leaving the node effectively dead, but # can still be inspected via JMX. # # stop_commit # shutdown the commit log, letting writes collect but # continuing to service reads, as in pre-2.0.5 Cassandra # # ignore # ignore fatal errors and let the batches fail commit_failure_policy: stop # Maximum size of the native protocol prepared statement cache # # Valid values are either "auto" (omitting the value) or a value greater 0. # # Note that specifying a too large value will result in long running GCs and possbily # out-of-memory errors. Keep the value at a small fraction of the heap. # # If you constantly see "prepared statements discarded in the last minute because # cache limit reached" messages, the first step is to investigate the root cause # of these messages and check whether prepared statements are used correctly - # i.e. use bind markers for variable parts. # # Do only change the default value, if you really have more prepared statements than # fit in the cache. In most cases it is not neccessary to change this value. # Constantly re-preparing statements is a performance penalty. # # Default value ("auto") is 1/256th of the heap or 10MB, whichever is greater prepared_statements_cache_size_mb: # Maximum size of the Thrift prepared statement cache # # If you do not use Thrift at all, it is safe to leave this value at "auto". # # See description of 'prepared_statements_cache_size_mb' above for more information. # # Default value ("auto") is 1/256th of the heap or 10MB, whichever is greater thrift_prepared_statements_cache_size_mb: # Maximum size of the key cache in memory. # # Each key cache hit saves 1 seek and each row cache hit saves 2 seeks at the # minimum, sometimes more. The key cache is fairly tiny for the amount of # time it saves, so it's worthwhile to use it at large numbers. # The row cache saves even more time, but must contain the entire row, # so it is extremely space-intensive. It's best to only use the # row cache if you have hot rows or static rows. # # NOTE: if you reduce the size, you may not get you hottest keys loaded on startup. # # Default value is empty to make it "auto" (min(5% of Heap (in MB), 100MB)). Set to 0 to disable key cache. key_cache_size_in_mb: # Duration in seconds after which Cassandra should # save the key cache. Caches are saved to saved_caches_directory as # specified in this configuration file. # # Saved caches greatly improve cold-start speeds, and is relatively cheap in # terms of I/O for the key cache. Row cache saving is much more expensive and # has limited use. # # Default is 14400 or 4 hours. key_cache_save_period: 14400 # Number of keys from the key cache to save # Disabled by default, meaning all keys are going to be saved # key_cache_keys_to_save: 100 # Row cache implementation class name. Available implementations: # # org.apache.cassandra.cache.OHCProvider # Fully off-heap row cache implementation (default). # # org.apache.cassandra.cache.SerializingCacheProvider # This is the row cache implementation availabile # in previous releases of Cassandra. # row_cache_class_name: org.apache.cassandra.cache.OHCProvider # Maximum size of the row cache in memory. # Please note that OHC cache implementation requires some additional off-heap memory to manage # the map structures and some in-flight memory during operations before/after cache entries can be # accounted against the cache capacity. This overhead is usually small compared to the whole capacity. # Do not specify more memory that the system can afford in the worst usual situation and leave some # headroom for OS block level cache. Do never allow your system to swap. # # Default value is 0, to disable row caching. row_cache_size_in_mb: 0 # Duration in seconds after which Cassandra should save the row cache. # Caches are saved to saved_caches_directory as specified in this configuration file. # # Saved caches greatly improve cold-start speeds, and is relatively cheap in # terms of I/O for the key cache. Row cache saving is much more expensive and # has limited use. # # Default is 0 to disable saving the row cache. row_cache_save_period: 0 # Number of keys from the row cache to save. # Specify 0 (which is the default), meaning all keys are going to be saved # row_cache_keys_to_save: 100 # Maximum size of the counter cache in memory. # # Counter cache helps to reduce counter locks' contention for hot counter cells. # In case of RF = 1 a counter cache hit will cause Cassandra to skip the read before # write entirely. With RF > 1 a counter cache hit will still help to reduce the duration # of the lock hold, helping with hot counter cell updates, but will not allow skipping # the read entirely. Only the local (clock, count) tuple of a counter cell is kept # in memory, not the whole counter, so it's relatively cheap. # # NOTE: if you reduce the size, you may not get you hottest keys loaded on startup. # # Default value is empty to make it "auto" (min(2.5% of Heap (in MB), 50MB)). Set to 0 to disable counter cache. # NOTE: if you perform counter deletes and rely on low gcgs, you should disable the counter cache. counter_cache_size_in_mb: # Duration in seconds after which Cassandra should # save the counter cache (keys only). Caches are saved to saved_caches_directory as # specified in this configuration file. # # Default is 7200 or 2 hours. counter_cache_save_period: 7200 # Number of keys from the counter cache to save # Disabled by default, meaning all keys are going to be saved # counter_cache_keys_to_save: 100 # saved caches # If not set, the default directory is $CASSANDRA_HOME/data/saved_caches. saved_caches_directory: /var/lib/cassandra/saved_caches # commitlog_sync may be either "periodic" or "batch." # # When in batch mode, Cassandra won't ack writes until the commit log # has been fsynced to disk. It will wait # commitlog_sync_batch_window_in_ms milliseconds between fsyncs. # This window should be kept short because the writer threads will # be unable to do extra work while waiting. (You may need to increase # concurrent_writes for the same reason.) # # commitlog_sync: batch # commitlog_sync_batch_window_in_ms: 2 # # the other option is "periodic" where writes may be acked immediately # and the CommitLog is simply synced every commitlog_sync_period_in_ms # milliseconds. commitlog_sync: periodic commitlog_sync_period_in_ms: 10000 # The size of the individual commitlog file segments. A commitlog # segment may be archived, deleted, or recycled once all the data # in it (potentially from each columnfamily in the system) has been # flushed to sstables. # # The default size is 32, which is almost always fine, but if you are # archiving commitlog segments (see commitlog_archiving.properties), # then you probably want a finer granularity of archiving; 8 or 16 MB # is reasonable. # Max mutation size is also configurable via max_mutation_size_in_kb setting in # cassandra.yaml. The default is half the size commitlog_segment_size_in_mb * 1024. # This should be positive and less than 2048. # # NOTE: If max_mutation_size_in_kb is set explicitly then commitlog_segment_size_in_mb must # be set to at least twice the size of max_mutation_size_in_kb / 1024 # commitlog_segment_size_in_mb: 32 # Compression to apply to the commit log. If omitted, the commit log # will be written uncompressed. LZ4, Snappy, and Deflate compressors # are supported. # commitlog_compression: # - class_name: LZ4Compressor # parameters: # - # any class that implements the SeedProvider interface and has a # constructor that takes a Map of parameters will do. seed_provider: # Addresses of hosts that are deemed contact points. # Cassandra nodes use this list of hosts to find each other and learn # the topology of the ring. You must change this if you are running # multiple nodes! - class_name: org.apache.cassandra.locator.SimpleSeedProvider parameters: # seeds is actually a comma-delimited list of addresses. # Ex: ",," - seeds: "172.17.0.2" # For workloads with more data than can fit in memory, Cassandra's # bottleneck will be reads that need to fetch data from # disk. "concurrent_reads" should be set to (16 * number_of_drives) in # order to allow the operations to enqueue low enough in the stack # that the OS and drives can reorder them. Same applies to # "concurrent_counter_writes", since counter writes read the current # values before incrementing and writing them back. # # On the other hand, since writes are almost never IO bound, the ideal # number of "concurrent_writes" is dependent on the number of cores in # your system; (8 * number_of_cores) is a good rule of thumb. concurrent_reads: 32 concurrent_writes: 32 concurrent_counter_writes: 32 # For materialized view writes, as there is a read involved, so this should # be limited by the less of concurrent reads or concurrent writes. concurrent_materialized_view_writes: 32 # Maximum memory to use for sstable chunk cache and buffer pooling. # 32MB of this are reserved for pooling buffers, the rest is used as an # cache that holds uncompressed sstable chunks. # Defaults to the smaller of 1/4 of heap or 512MB. This pool is allocated off-heap, # so is in addition to the memory allocated for heap. The cache also has on-heap # overhead which is roughly 128 bytes per chunk (i.e. 0.2% of the reserved size # if the default 64k chunk size is used). # Memory is only allocated when needed. # file_cache_size_in_mb: 512 # Flag indicating whether to allocate on or off heap when the sstable buffer # pool is exhausted, that is when it has exceeded the maximum memory # file_cache_size_in_mb, beyond which it will not cache buffers but allocate on request. # buffer_pool_use_heap_if_exhausted: true # The strategy for optimizing disk read # Possible values are: # ssd (for solid state disks, the default) # spinning (for spinning disks) # disk_optimization_strategy: ssd # Total permitted memory to use for memtables. Cassandra will stop # accepting writes when the limit is exceeded until a flush completes, # and will trigger a flush based on memtable_cleanup_threshold # If omitted, Cassandra will set both to 1/4 the size of the heap. # memtable_heap_space_in_mb: 2048 # memtable_offheap_space_in_mb: 2048 # memtable_cleanup_threshold is deprecated. The default calculation # is the only reasonable choice. See the comments on memtable_flush_writers # for more information. # # Ratio of occupied non-flushing memtable size to total permitted size # that will trigger a flush of the largest memtable. Larger mct will # mean larger flushes and hence less compaction, but also less concurrent # flush activity which can make it difficult to keep your disks fed # under heavy write load. # # memtable_cleanup_threshold defaults to 1 / (memtable_flush_writers + 1) # memtable_cleanup_threshold: 0.11 # Specify the way Cassandra allocates and manages memtable memory. # Options are: # # heap_buffers # on heap nio buffers # # offheap_buffers # off heap (direct) nio buffers # # offheap_objects # off heap objects memtable_allocation_type: heap_buffers # Total space to use for commit logs on disk. # # If space gets above this value, Cassandra will flush every dirty CF # in the oldest segment and remove it. So a small total commitlog space # will tend to cause more flush activity on less-active columnfamilies. # # The default value is the smaller of 8192, and 1/4 of the total space # of the commitlog volume. # # commitlog_total_space_in_mb: 8192 # This sets the number of memtable flush writer threads per disk # as well as the total number of memtables that can be flushed concurrently. # These are generally a combination of compute and IO bound. # # Memtable flushing is more CPU efficient than memtable ingest and a single thread # can keep up with the ingest rate of a whole server on a single fast disk # until it temporarily becomes IO bound under contention typically with compaction. # At that point you need multiple flush threads. At some point in the future # it may become CPU bound all the time. # # You can tell if flushing is falling behind using the MemtablePool.BlockedOnAllocation # metric which should be 0, but will be non-zero if threads are blocked waiting on flushing # to free memory. # # memtable_flush_writers defaults to two for a single data directory. # This means that two memtables can be flushed concurrently to the single data directory. # If you have multiple data directories the default is one memtable flushing at a time # but the flush will use a thread per data directory so you will get two or more writers. # # Two is generally enough to flush on a fast disk [array] mounted as a single data directory. # Adding more flush writers will result in smaller more frequent flushes that introduce more # compaction overhead. # # There is a direct tradeoff between number of memtables that can be flushed concurrently # and flush size and frequency. More is not better you just need enough flush writers # to never stall waiting for flushing to free memory. # #memtable_flush_writers: 2 # Total space to use for change-data-capture logs on disk. # # If space gets above this value, Cassandra will throw WriteTimeoutException # on Mutations including tables with CDC enabled. A CDCCompactor is responsible # for parsing the raw CDC logs and deleting them when parsing is completed. # # The default value is the min of 4096 mb and 1/8th of the total space # of the drive where cdc_raw_directory resides. # cdc_total_space_in_mb: 4096 # When we hit our cdc_raw limit and the CDCCompactor is either running behind # or experiencing backpressure, we check at the following interval to see if any # new space for cdc-tracked tables has been made available. Default to 250ms # cdc_free_space_check_interval_ms: 250 # A fixed memory pool size in MB for for SSTable index summaries. If left # empty, this will default to 5% of the heap size. If the memory usage of # all index summaries exceeds this limit, SSTables with low read rates will # shrink their index summaries in order to meet this limit. However, this # is a best-effort process. In extreme conditions Cassandra may need to use # more than this amount of memory. index_summary_capacity_in_mb: # How frequently index summaries should be resampled. This is done # periodically to redistribute memory from the fixed-size pool to sstables # proportional their recent read rates. Setting to -1 will disable this # process, leaving existing index summaries at their current sampling level. index_summary_resize_interval_in_minutes: 60 # Whether to, when doing sequential writing, fsync() at intervals in # order to force the operating system to flush the dirty # buffers. Enable this to avoid sudden dirty buffer flushing from # impacting read latencies. Almost always a good idea on SSDs; not # necessarily on platters. trickle_fsync: false trickle_fsync_interval_in_kb: 10240 # TCP port, for commands and data # For security reasons, you should not expose this port to the internet. Firewall it if needed. storage_port: 7000 # SSL port, for encrypted communication. Unused unless enabled in # encryption_options # For security reasons, you should not expose this port to the internet. Firewall it if needed. ssl_storage_port: 7001 # Address or interface to bind to and tell other Cassandra nodes to connect to. # You _must_ change this if you want multiple nodes to be able to communicate! # # Set listen_address OR listen_interface, not both. # # Leaving it blank leaves it up to InetAddress.getLocalHost(). This # will always do the Right Thing _if_ the node is properly configured # (hostname, name resolution, etc), and the Right Thing is to use the # address associated with the hostname (it might not be). # # Setting listen_address to 0.0.0.0 is always wrong. # listen_address: 172.17.0.2 # Set listen_address OR listen_interface, not both. Interfaces must correspond # to a single address, IP aliasing is not supported. # listen_interface: eth0 # If you choose to specify the interface by name and the interface has an ipv4 and an ipv6 address # you can specify which should be chosen using listen_interface_prefer_ipv6. If false the first ipv4 # address will be used. If true the first ipv6 address will be used. Defaults to false preferring # ipv4. If there is only one address it will be selected regardless of ipv4/ipv6. # listen_interface_prefer_ipv6: false # Address to broadcast to other Cassandra nodes # Leaving this blank will set it to the same value as listen_address broadcast_address: 172.17.0.2 # When using multiple physical network interfaces, set this # to true to listen on broadcast_address in addition to # the listen_address, allowing nodes to communicate in both # interfaces. # Ignore this property if the network configuration automatically # routes between the public and private networks such as EC2. # listen_on_broadcast_address: false # Internode authentication backend, implementing IInternodeAuthenticator; # used to allow/disallow connections from peer nodes. # internode_authenticator: org.apache.cassandra.auth.AllowAllInternodeAuthenticator # Whether to start the native transport server. # Please note that the address on which the native transport is bound is the # same as the rpc_address. The port however is different and specified below. start_native_transport: true # port for the CQL native transport to listen for clients on # For security reasons, you should not expose this port to the internet. Firewall it if needed. native_transport_port: 9042 # Enabling native transport encryption in client_encryption_options allows you to either use # encryption for the standard port or to use a dedicated, additional port along with the unencrypted # standard native_transport_port. # Enabling client encryption and keeping native_transport_port_ssl disabled will use encryption # for native_transport_port. Setting native_transport_port_ssl to a different value # from native_transport_port will use encryption for native_transport_port_ssl while # keeping native_transport_port unencrypted. # native_transport_port_ssl: 9142 # The maximum threads for handling requests when the native transport is used. # This is similar to rpc_max_threads though the default differs slightly (and # there is no native_transport_min_threads, idle threads will always be stopped # after 30 seconds). # native_transport_max_threads: 128 # # The maximum size of allowed frame. Frame (requests) larger than this will # be rejected as invalid. The default is 256MB. If you're changing this parameter, # you may want to adjust max_value_size_in_mb accordingly. This should be positive and less than 2048. # native_transport_max_frame_size_in_mb: 256 # The maximum number of concurrent client connections. # The default is -1, which means unlimited. # native_transport_max_concurrent_connections: -1 # The maximum number of concurrent client connections per source ip. # The default is -1, which means unlimited. # native_transport_max_concurrent_connections_per_ip: -1 # Whether to start the thrift rpc server. start_rpc: false # The address or interface to bind the Thrift RPC service and native transport # server to. # # Set rpc_address OR rpc_interface, not both. # # Leaving rpc_address blank has the same effect as on listen_address # (i.e. it will be based on the configured hostname of the node). # # Note that unlike listen_address, you can specify 0.0.0.0, but you must also # set broadcast_rpc_address to a value other than 0.0.0.0. # # For security reasons, you should not expose this port to the internet. Firewall it if needed. rpc_address: 0.0.0.0 # Set rpc_address OR rpc_interface, not both. Interfaces must correspond # to a single address, IP aliasing is not supported. # rpc_interface: eth1 # If you choose to specify the interface by name and the interface has an ipv4 and an ipv6 address # you can specify which should be chosen using rpc_interface_prefer_ipv6. If false the first ipv4 # address will be used. If true the first ipv6 address will be used. Defaults to false preferring # ipv4. If there is only one address it will be selected regardless of ipv4/ipv6. # rpc_interface_prefer_ipv6: false # port for Thrift to listen for clients on rpc_port: 9160 # RPC address to broadcast to drivers and other Cassandra nodes. This cannot # be set to 0.0.0.0. If left blank, this will be set to the value of # rpc_address. If rpc_address is set to 0.0.0.0, broadcast_rpc_address must # be set. broadcast_rpc_address: 172.17.0.2 # enable or disable keepalive on rpc/native connections rpc_keepalive: true # Cassandra provides two out-of-the-box options for the RPC Server: # # sync # One thread per thrift connection. For a very large number of clients, memory # will be your limiting factor. On a 64 bit JVM, 180KB is the minimum stack size # per thread, and that will correspond to your use of virtual memory (but physical memory # may be limited depending on use of stack space). # # hsha # Stands for "half synchronous, half asynchronous." All thrift clients are handled # asynchronously using a small number of threads that does not vary with the amount # of thrift clients (and thus scales well to many clients). The rpc requests are still # synchronous (one thread per active request). If hsha is selected then it is essential # that rpc_max_threads is changed from the default value of unlimited. # # The default is sync because on Windows hsha is about 30% slower. On Linux, # sync/hsha performance is about the same, with hsha of course using less memory. # # Alternatively, can provide your own RPC server by providing the fully-qualified class name # of an o.a.c.t.TServerFactory that can create an instance of it. rpc_server_type: sync # Uncomment rpc_min|max_thread to set request pool size limits. # # Regardless of your choice of RPC server (see above), the number of maximum requests in the # RPC thread pool dictates how many concurrent requests are possible (but if you are using the sync # RPC server, it also dictates the number of clients that can be connected at all). # # The default is unlimited and thus provides no protection against clients overwhelming the server. You are # encouraged to set a maximum that makes sense for you in production, but do keep in mind that # rpc_max_threads represents the maximum number of client requests this server may execute concurrently. # # rpc_min_threads: 16 # rpc_max_threads: 2048 # uncomment to set socket buffer sizes on rpc connections # rpc_send_buff_size_in_bytes: # rpc_recv_buff_size_in_bytes: # Uncomment to set socket buffer size for internode communication # Note that when setting this, the buffer size is limited by net.core.wmem_max # and when not setting it it is defined by net.ipv4.tcp_wmem # See also: # /proc/sys/net/core/wmem_max # /proc/sys/net/core/rmem_max # /proc/sys/net/ipv4/tcp_wmem # /proc/sys/net/ipv4/tcp_wmem # and 'man tcp' # internode_send_buff_size_in_bytes: # Uncomment to set socket buffer size for internode communication # Note that when setting this, the buffer size is limited by net.core.wmem_max # and when not setting it it is defined by net.ipv4.tcp_wmem # internode_recv_buff_size_in_bytes: # Frame size for thrift (maximum message length). thrift_framed_transport_size_in_mb: 15 # Set to true to have Cassandra create a hard link to each sstable # flushed or streamed locally in a backups/ subdirectory of the # keyspace data. Removing these links is the operator's # responsibility. incremental_backups: false # Whether or not to take a snapshot before each compaction. Be # careful using this option, since Cassandra won't clean up the # snapshots for you. Mostly useful if you're paranoid when there # is a data format change. snapshot_before_compaction: false # Whether or not a snapshot is taken of the data before keyspace truncation # or dropping of column families. The STRONGLY advised default of true # should be used to provide data safety. If you set this flag to false, you will # lose data on truncation or drop. auto_snapshot: true # Granularity of the collation index of rows within a partition. # Increase if your rows are large, or if you have a very large # number of rows per partition. The competing goals are these: # # - a smaller granularity means more index entries are generated # and looking up rows withing the partition by collation column # is faster # - but, Cassandra will keep the collation index in memory for hot # rows (as part of the key cache), so a larger granularity means # you can cache more hot rows column_index_size_in_kb: 64 # Per sstable indexed key cache entries (the collation index in memory # mentioned above) exceeding this size will not be held on heap. # This means that only partition information is held on heap and the # index entries are read from disk. # # Note that this size refers to the size of the # serialized index information and not the size of the partition. column_index_cache_size_in_kb: 2 # Number of simultaneous compactions to allow, NOT including # validation "compactions" for anti-entropy repair. Simultaneous # compactions can help preserve read performance in a mixed read/write # workload, by mitigating the tendency of small sstables to accumulate # during a single long running compactions. The default is usually # fine and if you experience problems with compaction running too # slowly or too fast, you should look at # compaction_throughput_mb_per_sec first. # # concurrent_compactors defaults to the smaller of (number of disks, # number of cores), with a minimum of 2 and a maximum of 8. # # If your data directories are backed by SSD, you should increase this # to the number of cores. #concurrent_compactors: 1 # Throttles compaction to the given total throughput across the entire # system. The faster you insert data, the faster you need to compact in # order to keep the sstable count down, but in general, setting this to # 16 to 32 times the rate you are inserting data is more than sufficient. # Setting this to 0 disables throttling. Note that this account for all types # of compaction, including validation compaction. compaction_throughput_mb_per_sec: 16 # When compacting, the replacement sstable(s) can be opened before they # are completely written, and used in place of the prior sstables for # any range that has been written. This helps to smoothly transfer reads # between the sstables, reducing page cache churn and keeping hot rows hot sstable_preemptive_open_interval_in_mb: 50 # Throttles all outbound streaming file transfers on this node to the # given total throughput in Mbps. This is necessary because Cassandra does # mostly sequential IO when streaming data during bootstrap or repair, which # can lead to saturating the network connection and degrading rpc performance. # When unset, the default is 200 Mbps or 25 MB/s. # stream_throughput_outbound_megabits_per_sec: 200 # Throttles all streaming file transfer between the datacenters, # this setting allows users to throttle inter dc stream throughput in addition # to throttling all network stream traffic as configured with # stream_throughput_outbound_megabits_per_sec # When unset, the default is 200 Mbps or 25 MB/s # inter_dc_stream_throughput_outbound_megabits_per_sec: 200 # How long the coordinator should wait for read operations to complete read_request_timeout_in_ms: 5000 # How long the coordinator should wait for seq or index scans to complete range_request_timeout_in_ms: 10000 # How long the coordinator should wait for writes to complete write_request_timeout_in_ms: 2000 # How long the coordinator should wait for counter writes to complete counter_write_request_timeout_in_ms: 5000 # How long a coordinator should continue to retry a CAS operation # that contends with other proposals for the same row cas_contention_timeout_in_ms: 1000 # How long the coordinator should wait for truncates to complete # (This can be much longer, because unless auto_snapshot is disabled # we need to flush first so we can snapshot before removing the data.) truncate_request_timeout_in_ms: 60000 # The default timeout for other, miscellaneous operations request_timeout_in_ms: 10000 # How long before a node logs slow queries. Select queries that take longer than # this timeout to execute, will generate an aggregated log message, so that slow queries # can be identified. Set this value to zero to disable slow query logging. slow_query_log_timeout_in_ms: 500 # Enable operation timeout information exchange between nodes to accurately # measure request timeouts. If disabled, replicas will assume that requests # were forwarded to them instantly by the coordinator, which means that # under overload conditions we will waste that much extra time processing # already-timed-out requests. # # Warning: before enabling this property make sure to ntp is installed # and the times are synchronized between the nodes. cross_node_timeout: false # Set keep-alive period for streaming # This node will send a keep-alive message periodically with this period. # If the node does not receive a keep-alive message from the peer for # 2 keep-alive cycles the stream session times out and fail # Default value is 300s (5 minutes), which means stalled stream # times out in 10 minutes by default # streaming_keep_alive_period_in_secs: 300 # phi value that must be reached for a host to be marked down. # most users should never need to adjust this. # phi_convict_threshold: 8 # endpoint_snitch -- Set this to a class that implements # IEndpointSnitch. The snitch has two functions: # # - it teaches Cassandra enough about your network topology to route # requests efficiently # - it allows Cassandra to spread replicas around your cluster to avoid # correlated failures. It does this by grouping machines into # "datacenters" and "racks." Cassandra will do its best not to have # more than one replica on the same "rack" (which may not actually # be a physical location) # # CASSANDRA WILL NOT ALLOW YOU TO SWITCH TO AN INCOMPATIBLE SNITCH # ONCE DATA IS INSERTED INTO THE CLUSTER. This would cause data loss. # This means that if you start with the default SimpleSnitch, which # locates every node on "rack1" in "datacenter1", your only options # if you need to add another datacenter are GossipingPropertyFileSnitch # (and the older PFS). From there, if you want to migrate to an # incompatible snitch like Ec2Snitch you can do it by adding new nodes # under Ec2Snitch (which will locate them in a new "datacenter") and # decommissioning the old ones. # # Out of the box, Cassandra provides: # # SimpleSnitch: # Treats Strategy order as proximity. This can improve cache # locality when disabling read repair. Only appropriate for # single-datacenter deployments. # # GossipingPropertyFileSnitch # This should be your go-to snitch for production use. The rack # and datacenter for the local node are defined in # cassandra-rackdc.properties and propagated to other nodes via # gossip. If cassandra-topology.properties exists, it is used as a # fallback, allowing migration from the PropertyFileSnitch. # # PropertyFileSnitch: # Proximity is determined by rack and data center, which are # explicitly configured in cassandra-topology.properties. # # Ec2Snitch: # Appropriate for EC2 deployments in a single Region. Loads Region # and Availability Zone information from the EC2 API. The Region is # treated as the datacenter, and the Availability Zone as the rack. # Only private IPs are used, so this will not work across multiple # Regions. # # Ec2MultiRegionSnitch: # Uses public IPs as broadcast_address to allow cross-region # connectivity. (Thus, you should set seed addresses to the public # IP as well.) You will need to open the storage_port or # ssl_storage_port on the public IP firewall. (For intra-Region # traffic, Cassandra will switch to the private IP after # establishing a connection.) # # RackInferringSnitch: # Proximity is determined by rack and data center, which are # assumed to correspond to the 3rd and 2nd octet of each node's IP # address, respectively. Unless this happens to match your # deployment conventions, this is best used as an example of # writing a custom Snitch class and is provided in that spirit. # # You can use a custom Snitch by setting this to the full class name # of the snitch, which will be assumed to be on your classpath. endpoint_snitch: SimpleSnitch # controls how often to perform the more expensive part of host score # calculation dynamic_snitch_update_interval_in_ms: 100 # controls how often to reset all host scores, allowing a bad host to # possibly recover dynamic_snitch_reset_interval_in_ms: 600000 # if set greater than zero and read_repair_chance is < 1.0, this will allow # 'pinning' of replicas to hosts in order to increase cache capacity. # The badness threshold will control how much worse the pinned host has to be # before the dynamic snitch will prefer other replicas over it. This is # expressed as a double which represents a percentage. Thus, a value of # 0.2 means Cassandra would continue to prefer the static snitch values # until the pinned host was 20% worse than the fastest. dynamic_snitch_badness_threshold: 0.1 # request_scheduler -- Set this to a class that implements # RequestScheduler, which will schedule incoming client requests # according to the specific policy. This is useful for multi-tenancy # with a single Cassandra cluster. # NOTE: This is specifically for requests from the client and does # not affect inter node communication. # org.apache.cassandra.scheduler.NoScheduler - No scheduling takes place # org.apache.cassandra.scheduler.RoundRobinScheduler - Round robin of # client requests to a node with a separate queue for each # request_scheduler_id. The scheduler is further customized by # request_scheduler_options as described below. request_scheduler: org.apache.cassandra.scheduler.NoScheduler # Scheduler Options vary based on the type of scheduler # # NoScheduler # Has no options # # RoundRobin # throttle_limit # The throttle_limit is the number of in-flight # requests per client. Requests beyond # that limit are queued up until # running requests can complete. # The value of 80 here is twice the number of # concurrent_reads + concurrent_writes. # default_weight # default_weight is optional and allows for # overriding the default which is 1. # weights # Weights are optional and will default to 1 or the # overridden default_weight. The weight translates into how # many requests are handled during each turn of the # RoundRobin, based on the scheduler id. # # request_scheduler_options: # throttle_limit: 80 # default_weight: 5 # weights: # Keyspace1: 1 # Keyspace2: 5 # request_scheduler_id -- An identifier based on which to perform # the request scheduling. Currently the only valid option is keyspace. # request_scheduler_id: keyspace # Enable or disable inter-node encryption # JVM defaults for supported SSL socket protocols and cipher suites can # be replaced using custom encryption options. This is not recommended # unless you have policies in place that dictate certain settings, or # need to disable vulnerable ciphers or protocols in case the JVM cannot # be updated. # FIPS compliant settings can be configured at JVM level and should not # involve changing encryption settings here: # https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/FIPS.html # *NOTE* No custom encryption options are enabled at the moment # The available internode options are : all, none, dc, rack # # If set to dc cassandra will encrypt the traffic between the DCs # If set to rack cassandra will encrypt the traffic between the racks # # The passwords used in these options must match the passwords used when generating # the keystore and truststore. For instructions on generating these files, see: # http://download.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html#CreateKeystore # server_encryption_options: internode_encryption: none keystore: conf/keystore keystore_password: cassandra truststore: conf/.truststore truststore_password: cassandra # More advanced defaults below: # protocol: TLS # algorithm: SunX509 # store_type: JKS # cipher_suites: [TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA] # require_client_auth: false # require_endpoint_verification: false # enable or disable client/server encryption. client_encryption_options: enabled: true # If enabled and optional is set to true encrypted and unencrypted connections are handled. optional: false keystore: /etc/cassandra/keystore.p12 keystore_password: "cassandra" require_client_auth: true truststore: /etc/cassandra/truststore.p12 truststore_password: "cassandra" store_type: PKCS12 # More advanced defaults below: # protocol: TLS # algorithm: SunX509 # store_type: JKS # cipher_suites: [TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA] # internode_compression controls whether traffic between nodes is # compressed. # Can be: # # all # all traffic is compressed # # dc # traffic between different datacenters is compressed # # none # nothing is compressed. internode_compression: dc # Enable or disable tcp_nodelay for inter-dc communication. # Disabling it will result in larger (but fewer) network packets being sent, # reducing overhead from the TCP protocol itself, at the cost of increasing # latency if you block for cross-datacenter responses. inter_dc_tcp_nodelay: false # TTL for different trace types used during logging of the repair process. tracetype_query_ttl: 86400 tracetype_repair_ttl: 604800 # By default, Cassandra logs GC Pauses greater than 200 ms at INFO level # This threshold can be adjusted to minimize logging if necessary # gc_log_threshold_in_ms: 200 # If unset, all GC Pauses greater than gc_log_threshold_in_ms will log at # INFO level # UDFs (user defined functions) are disabled by default. # As of Cassandra 3.0 there is a sandbox in place that should prevent execution of evil code. enable_user_defined_functions: false # Enables scripted UDFs (JavaScript UDFs). # Java UDFs are always enabled, if enable_user_defined_functions is true. # Enable this option to be able to use UDFs with "language javascript" or any custom JSR-223 provider. # This option has no effect, if enable_user_defined_functions is false. enable_scripted_user_defined_functions: false # The default Windows kernel timer and scheduling resolution is 15.6ms for power conservation. # Lowering this value on Windows can provide much tighter latency and better throughput, however # some virtualized environments may see a negative performance impact from changing this setting # below their system default. The sysinternals 'clockres' tool can confirm your system's default # setting. windows_timer_interval: 1 # Enables encrypting data at-rest (on disk). Different key providers can be plugged in, but the default reads from # a JCE-style keystore. A single keystore can hold multiple keys, but the one referenced by # the "key_alias" is the only key that will be used for encrypt opertaions; previously used keys # can still (and should!) be in the keystore and will be used on decrypt operations # (to handle the case of key rotation). # # It is strongly recommended to download and install Java Cryptography Extension (JCE) # Unlimited Strength Jurisdiction Policy Files for your version of the JDK. # (current link: http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html) # # Currently, only the following file types are supported for transparent data encryption, although # more are coming in future cassandra releases: commitlog, hints transparent_data_encryption_options: enabled: false chunk_length_kb: 64 cipher: AES/CBC/PKCS5Padding key_alias: testing:1 # CBC IV length for AES needs to be 16 bytes (which is also the default size) # iv_length: 16 key_provider: - class_name: org.apache.cassandra.security.JKSKeyProvider parameters: - keystore: conf/keystore keystore_password: cassandra store_type: JCEKS key_password: cassandra ##################### # SAFETY THRESHOLDS # ##################### # When executing a scan, within or across a partition, we need to keep the # tombstones seen in memory so we can return them to the coordinator, which # will use them to make sure other replicas also know about the deleted rows. # With workloads that generate a lot of tombstones, this can cause performance # problems and even exaust the server heap. # (http://www.datastax.com/dev/blog/cassandra-anti-patterns-queues-and-queue-like-datasets) # Adjust the thresholds here if you understand the dangers and want to # scan more tombstones anyway. These thresholds may also be adjusted at runtime # using the StorageService mbean. tombstone_warn_threshold: 1000 tombstone_failure_threshold: 100000 # Log WARN on any multiple-partition batch size exceeding this value. 5kb per batch by default. # Caution should be taken on increasing the size of this threshold as it can lead to node instability. batch_size_warn_threshold_in_kb: 5 # Fail any multiple-partition batch exceeding this value. 50kb (10x warn threshold) by default. batch_size_fail_threshold_in_kb: 50 # Log WARN on any batches not of type LOGGED than span across more partitions than this limit unlogged_batch_across_partitions_warn_threshold: 10 # Log a warning when compacting partitions larger than this value compaction_large_partition_warning_threshold_mb: 100 # GC Pauses greater than gc_warn_threshold_in_ms will be logged at WARN level # Adjust the threshold based on your application throughput requirement # By default, Cassandra logs GC Pauses greater than 200 ms at INFO level gc_warn_threshold_in_ms: 1000 # Maximum size of any value in SSTables. Safety measure to detect SSTable corruption # early. Any value size larger than this threshold will result into marking an SSTable # as corrupted. This should be positive and less than 2048. # max_value_size_in_mb: 256 # Back-pressure settings # # If enabled, the coordinator will apply the back-pressure strategy specified below to each mutation # sent to replicas, with the aim of reducing pressure on overloaded replicas. back_pressure_enabled: false # The back-pressure strategy applied. # The default implementation, RateBasedBackPressure, takes three arguments: # high ratio, factor, and flow type, and uses the ratio between incoming mutation responses and outgoing mutation requests. # If below high ratio, outgoing mutations are rate limited according to the incoming rate decreased by the given factor; # if above high ratio, the rate limiting is increased by the given factor; # such factor is usually best configured between 1 and 10, use larger values for a faster recovery # at the expense of potentially more dropped mutations; # the rate limiting is applied according to the flow type: if FAST, it's rate limited at the speed of the fastest replica, # if SLOW at the speed of the slowest one. # New strategies can be added. Implementors need to implement org.apache.cassandra.net.BackpressureStrategy and # provide a public constructor accepting a Map. back_pressure_strategy: - class_name: org.apache.cassandra.net.RateBasedBackPressure parameters: - high_ratio: 0.90 factor: 5 flow: FAST # Coalescing Strategies # # Coalescing multiples messages turns out to significantly boost message processing throughput (think doubling or more). # On bare metal, the floor for packet processing throughput is high enough that many applications won't notice, but in # virtualized environments, the point at which an application can be bound by network packet processing can be # surprisingly low compared to the throughput of task processing that is possible inside a VM. It's not that bare metal # doesn't benefit from coalescing messages, it's that the number of packets a bare metal network interface can process # is sufficient for many applications such that no load starvation is experienced even without coalescing. # There are other benefits to coalescing network messages that are harder to isolate with a simple metric like messages # per second. By coalescing multiple tasks together, a network thread can process multiple messages for the cost of one # trip to read from a socket, and all the task submission work can be done at the same time reducing context switching # and increasing cache friendliness of network message processing. # See CASSANDRA-8692 for details. # Strategy to use for coalescing messages in OutboundTcpConnection. # Can be fixed, movingaverage, timehorizon, disabled (default). # You can also specify a subclass of CoalescingStrategies.CoalescingStrategy by name. # otc_coalescing_strategy: DISABLED # How many microseconds to wait for coalescing. For fixed strategy this is the amount of time after the first # message is received before it will be sent with any accompanying messages. For moving average this is the # maximum amount of time that will be waited as well as the interval at which messages must arrive on average # for coalescing to be enabled. # otc_coalescing_window_us: 200 # Do not try to coalesce messages if we already got that many messages. This should be more than 2 and less than 128. # otc_coalescing_enough_coalesced_messages: 8 # How many milliseconds to wait between two expiration runs on the backlog (queue) of the OutboundTcpConnection. # Expiration is done if messages are piling up in the backlog. Droppable messages are expired to free the memory # taken by expired messages. The interval should be between 0 and 1000, and in most installations the default value # will be appropriate. A smaller value could potentially expire messages slightly sooner at the expense of more CPU # time and queue contention while iterating the backlog of messages. # An interval of 0 disables any wait time, which is the behavior of former Cassandra versions. # # otc_backlog_expiration_interval_ms: 200 ================================================ FILE: modules/cassandra/src/test/resources/cassandra-test-configuration-example/cassandra.yaml ================================================ # Cassandra storage config YAML # NOTE: # See http://wiki.apache.org/cassandra/StorageConfiguration for # full explanations of configuration directives # /NOTE # The name of the cluster. This is mainly used to prevent machines in # one logical cluster from joining another. cluster_name: 'Test Cluster Integration Test' # This defines the number of tokens randomly assigned to this node on the ring # The more tokens, relative to other nodes, the larger the proportion of data # that this node will store. You probably want all nodes to have the same number # of tokens assuming they have equal hardware capability. # # If you leave this unspecified, Cassandra will use the default of 1 token for legacy compatibility, # and will use the initial_token as described below. # # Specifying initial_token will override this setting on the node's initial start, # on subsequent starts, this setting will apply even if initial token is set. # # If you already have a cluster with 1 token per node, and wish to migrate to # multiple tokens per node, see http://wiki.apache.org/cassandra/Operations num_tokens: 256 # Triggers automatic allocation of num_tokens tokens for this node. The allocation # algorithm attempts to choose tokens in a way that optimizes replicated load over # the nodes in the datacenter for the replication strategy used by the specified # keyspace. # # The load assigned to each node will be close to proportional to its number of # vnodes. # # Only supported with the Murmur3Partitioner. # allocate_tokens_for_keyspace: KEYSPACE # initial_token allows you to specify tokens manually. While you can use it with # vnodes (num_tokens > 1, above) -- in which case you should provide a # comma-separated list -- it's primarily used when adding nodes to legacy clusters # that do not have vnodes enabled. # initial_token: # See http://wiki.apache.org/cassandra/HintedHandoff # May either be "true" or "false" to enable globally hinted_handoff_enabled: true # When hinted_handoff_enabled is true, a black list of data centers that will not # perform hinted handoff # hinted_handoff_disabled_datacenters: # - DC1 # - DC2 # this defines the maximum amount of time a dead host will have hints # generated. After it has been dead this long, new hints for it will not be # created until it has been seen alive and gone down again. max_hint_window_in_ms: 10800000 # 3 hours # Maximum throttle in KBs per second, per delivery thread. This will be # reduced proportionally to the number of nodes in the cluster. (If there # are two nodes in the cluster, each delivery thread will use the maximum # rate; if there are three, each will throttle to half of the maximum, # since we expect two nodes to be delivering hints simultaneously.) hinted_handoff_throttle_in_kb: 1024 # Number of threads with which to deliver hints; # Consider increasing this number when you have multi-dc deployments, since # cross-dc handoff tends to be slower max_hints_delivery_threads: 2 # Directory where Cassandra should store hints. # If not set, the default directory is $CASSANDRA_HOME/data/hints. # hints_directory: /var/lib/cassandra/hints # How often hints should be flushed from the internal buffers to disk. # Will *not* trigger fsync. hints_flush_period_in_ms: 10000 # Maximum size for a single hints file, in megabytes. max_hints_file_size_in_mb: 128 # Compression to apply to the hint files. If omitted, hints files # will be written uncompressed. LZ4, Snappy, and Deflate compressors # are supported. #hints_compression: # - class_name: LZ4Compressor # parameters: # - # Maximum throttle in KBs per second, total. This will be # reduced proportionally to the number of nodes in the cluster. batchlog_replay_throttle_in_kb: 1024 # Authentication backend, implementing IAuthenticator; used to identify users # Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthenticator, # PasswordAuthenticator}. # # - AllowAllAuthenticator performs no checks - set it to disable authentication. # - PasswordAuthenticator relies on username/password pairs to authenticate # users. It keeps usernames and hashed passwords in system_auth.roles table. # Please increase system_auth keyspace replication factor if you use this authenticator. # If using PasswordAuthenticator, CassandraRoleManager must also be used (see below) authenticator: AllowAllAuthenticator # Authorization backend, implementing IAuthorizer; used to limit access/provide permissions # Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthorizer, # CassandraAuthorizer}. # # - AllowAllAuthorizer allows any action to any user - set it to disable authorization. # - CassandraAuthorizer stores permissions in system_auth.role_permissions table. Please # increase system_auth keyspace replication factor if you use this authorizer. authorizer: AllowAllAuthorizer # Part of the Authentication & Authorization backend, implementing IRoleManager; used # to maintain grants and memberships between roles. # Out of the box, Cassandra provides org.apache.cassandra.auth.CassandraRoleManager, # which stores role information in the system_auth keyspace. Most functions of the # IRoleManager require an authenticated login, so unless the configured IAuthenticator # actually implements authentication, most of this functionality will be unavailable. # # - CassandraRoleManager stores role data in the system_auth keyspace. Please # increase system_auth keyspace replication factor if you use this role manager. role_manager: CassandraRoleManager # Validity period for roles cache (fetching granted roles can be an expensive # operation depending on the role manager, CassandraRoleManager is one example) # Granted roles are cached for authenticated sessions in AuthenticatedUser and # after the period specified here, become eligible for (async) reload. # Defaults to 2000, set to 0 to disable caching entirely. # Will be disabled automatically for AllowAllAuthenticator. roles_validity_in_ms: 2000 # Refresh interval for roles cache (if enabled). # After this interval, cache entries become eligible for refresh. Upon next # access, an async reload is scheduled and the old value returned until it # completes. If roles_validity_in_ms is non-zero, then this must be # also. # Defaults to the same value as roles_validity_in_ms. # roles_update_interval_in_ms: 2000 # Validity period for permissions cache (fetching permissions can be an # expensive operation depending on the authorizer, CassandraAuthorizer is # one example). Defaults to 2000, set to 0 to disable. # Will be disabled automatically for AllowAllAuthorizer. permissions_validity_in_ms: 2000 # Refresh interval for permissions cache (if enabled). # After this interval, cache entries become eligible for refresh. Upon next # access, an async reload is scheduled and the old value returned until it # completes. If permissions_validity_in_ms is non-zero, then this must be # also. # Defaults to the same value as permissions_validity_in_ms. # permissions_update_interval_in_ms: 2000 # Validity period for credentials cache. This cache is tightly coupled to # the provided PasswordAuthenticator implementation of IAuthenticator. If # another IAuthenticator implementation is configured, this cache will not # be automatically used and so the following settings will have no effect. # Please note, credentials are cached in their encrypted form, so while # activating this cache may reduce the number of queries made to the # underlying table, it may not bring a significant reduction in the # latency of individual authentication attempts. # Defaults to 2000, set to 0 to disable credentials caching. credentials_validity_in_ms: 2000 # Refresh interval for credentials cache (if enabled). # After this interval, cache entries become eligible for refresh. Upon next # access, an async reload is scheduled and the old value returned until it # completes. If credentials_validity_in_ms is non-zero, then this must be # also. # Defaults to the same value as credentials_validity_in_ms. # credentials_update_interval_in_ms: 2000 # The partitioner is responsible for distributing groups of rows (by # partition key) across nodes in the cluster. You should leave this # alone for new clusters. The partitioner can NOT be changed without # reloading all data, so when upgrading you should set this to the # same partitioner you were already using. # # Besides Murmur3Partitioner, partitioners included for backwards # compatibility include RandomPartitioner, ByteOrderedPartitioner, and # OrderPreservingPartitioner. # partitioner: org.apache.cassandra.dht.Murmur3Partitioner # Directories where Cassandra should store data on disk. Cassandra # will spread data evenly across them, subject to the granularity of # the configured compaction strategy. # If not set, the default directory is $CASSANDRA_HOME/data/data. data_file_directories: - /var/lib/cassandra/data # commit log. when running on magnetic HDD, this should be a # separate spindle than the data directories. # If not set, the default directory is $CASSANDRA_HOME/data/commitlog. commitlog_directory: /var/lib/cassandra/commitlog # Enable / disable CDC functionality on a per-node basis. This modifies the logic used # for write path allocation rejection (standard: never reject. cdc: reject Mutation # containing a CDC-enabled table if at space limit in cdc_raw_directory). cdc_enabled: false # CommitLogSegments are moved to this directory on flush if cdc_enabled: true and the # segment contains mutations for a CDC-enabled table. This should be placed on a # separate spindle than the data directories. If not set, the default directory is # $CASSANDRA_HOME/data/cdc_raw. # cdc_raw_directory: /var/lib/cassandra/cdc_raw # Policy for data disk failures: # # die # shut down gossip and client transports and kill the JVM for any fs errors or # single-sstable errors, so the node can be replaced. # # stop_paranoid # shut down gossip and client transports even for single-sstable errors, # kill the JVM for errors during startup. # # stop # shut down gossip and client transports, leaving the node effectively dead, but # can still be inspected via JMX, kill the JVM for errors during startup. # # best_effort # stop using the failed disk and respond to requests based on # remaining available sstables. This means you WILL see obsolete # data at CL.ONE! # # ignore # ignore fatal errors and let requests fail, as in pre-1.2 Cassandra disk_failure_policy: stop # Policy for commit disk failures: # # die # shut down gossip and Thrift and kill the JVM, so the node can be replaced. # # stop # shut down gossip and Thrift, leaving the node effectively dead, but # can still be inspected via JMX. # # stop_commit # shutdown the commit log, letting writes collect but # continuing to service reads, as in pre-2.0.5 Cassandra # # ignore # ignore fatal errors and let the batches fail commit_failure_policy: stop # Maximum size of the native protocol prepared statement cache # # Valid values are either "auto" (omitting the value) or a value greater 0. # # Note that specifying a too large value will result in long running GCs and possibly # out-of-memory errors. Keep the value at a small fraction of the heap. # # If you constantly see "prepared statements discarded in the last minute because # cache limit reached" messages, the first step is to investigate the root cause # of these messages and check whether prepared statements are used correctly - # i.e. use bind markers for variable parts. # # Do only change the default value, if you really have more prepared statements than # fit in the cache. In most cases it is not necessary to change this value. # Constantly re-preparing statements is a performance penalty. # # Default value ("auto") is 1/256th of the heap or 10MB, whichever is greater prepared_statements_cache_size_mb: # Maximum size of the Thrift prepared statement cache # # If you do not use Thrift at all, it is safe to leave this value at "auto". # # See description of 'prepared_statements_cache_size_mb' above for more information. # # Default value ("auto") is 1/256th of the heap or 10MB, whichever is greater thrift_prepared_statements_cache_size_mb: # Maximum size of the key cache in memory. # # Each key cache hit saves 1 seek and each row cache hit saves 2 seeks at the # minimum, sometimes more. The key cache is fairly tiny for the amount of # time it saves, so it's worthwhile to use it at large numbers. # The row cache saves even more time, but must contain the entire row, # so it is extremely space-intensive. It's best to only use the # row cache if you have hot rows or static rows. # # NOTE: if you reduce the size, you may not get you hottest keys loaded on startup. # # Default value is empty to make it "auto" (min(5% of Heap (in MB), 100MB)). Set to 0 to disable key cache. key_cache_size_in_mb: # Duration in seconds after which Cassandra should # save the key cache. Caches are saved to saved_caches_directory as # specified in this configuration file. # # Saved caches greatly improve cold-start speeds, and is relatively cheap in # terms of I/O for the key cache. Row cache saving is much more expensive and # has limited use. # # Default is 14400 or 4 hours. key_cache_save_period: 14400 # Number of keys from the key cache to save # Disabled by default, meaning all keys are going to be saved # key_cache_keys_to_save: 100 # Row cache implementation class name. Available implementations: # # org.apache.cassandra.cache.OHCProvider # Fully off-heap row cache implementation (default). # # org.apache.cassandra.cache.SerializingCacheProvider # This is the row cache implementation available # in previous releases of Cassandra. # row_cache_class_name: org.apache.cassandra.cache.OHCProvider # Maximum size of the row cache in memory. # Please note that OHC cache implementation requires some additional off-heap memory to manage # the map structures and some in-flight memory during operations before/after cache entries can be # accounted against the cache capacity. This overhead is usually small compared to the whole capacity. # Do not specify more memory that the system can afford in the worst usual situation and leave some # headroom for OS block level cache. Do never allow your system to swap. # # Default value is 0, to disable row caching. row_cache_size_in_mb: 0 # Duration in seconds after which Cassandra should save the row cache. # Caches are saved to saved_caches_directory as specified in this configuration file. # # Saved caches greatly improve cold-start speeds, and is relatively cheap in # terms of I/O for the key cache. Row cache saving is much more expensive and # has limited use. # # Default is 0 to disable saving the row cache. row_cache_save_period: 0 # Number of keys from the row cache to save. # Specify 0 (which is the default), meaning all keys are going to be saved # row_cache_keys_to_save: 100 # Maximum size of the counter cache in memory. # # Counter cache helps to reduce counter locks' contention for hot counter cells. # In case of RF = 1 a counter cache hit will cause Cassandra to skip the read before # write entirely. With RF > 1 a counter cache hit will still help to reduce the duration # of the lock hold, helping with hot counter cell updates, but will not allow skipping # the read entirely. Only the local (clock, count) tuple of a counter cell is kept # in memory, not the whole counter, so it's relatively cheap. # # NOTE: if you reduce the size, you may not get you hottest keys loaded on startup. # # Default value is empty to make it "auto" (min(2.5% of Heap (in MB), 50MB)). Set to 0 to disable counter cache. # NOTE: if you perform counter deletes and rely on low gcgs, you should disable the counter cache. counter_cache_size_in_mb: # Duration in seconds after which Cassandra should # save the counter cache (keys only). Caches are saved to saved_caches_directory as # specified in this configuration file. # # Default is 7200 or 2 hours. counter_cache_save_period: 7200 # Number of keys from the counter cache to save # Disabled by default, meaning all keys are going to be saved # counter_cache_keys_to_save: 100 # saved caches # If not set, the default directory is $CASSANDRA_HOME/data/saved_caches. saved_caches_directory: /var/lib/cassandra/saved_caches # commitlog_sync may be either "periodic" or "batch." # # When in batch mode, Cassandra won't ack writes until the commit log # has been fsynced to disk. It will wait # commitlog_sync_batch_window_in_ms milliseconds between fsyncs. # This window should be kept short because the writer threads will # be unable to do extra work while waiting. (You may need to increase # concurrent_writes for the same reason.) # # commitlog_sync: batch # commitlog_sync_batch_window_in_ms: 2 # # the other option is "periodic" where writes may be acked immediately # and the CommitLog is simply synced every commitlog_sync_period_in_ms # milliseconds. commitlog_sync: periodic commitlog_sync_period_in_ms: 10000 # The size of the individual commitlog file segments. A commitlog # segment may be archived, deleted, or recycled once all the data # in it (potentially from each columnfamily in the system) has been # flushed to sstables. # # The default size is 32, which is almost always fine, but if you are # archiving commitlog segments (see commitlog_archiving.properties), # then you probably want a finer granularity of archiving; 8 or 16 MB # is reasonable. # Max mutation size is also configurable via max_mutation_size_in_kb setting in # cassandra.yaml. The default is half the size commitlog_segment_size_in_mb * 1024. # This should be positive and less than 2048. # # NOTE: If max_mutation_size_in_kb is set explicitly then commitlog_segment_size_in_mb must # be set to at least twice the size of max_mutation_size_in_kb / 1024 # commitlog_segment_size_in_mb: 32 # Compression to apply to the commit log. If omitted, the commit log # will be written uncompressed. LZ4, Snappy, and Deflate compressors # are supported. # commitlog_compression: # - class_name: LZ4Compressor # parameters: # - # any class that implements the SeedProvider interface and has a # constructor that takes a Map of parameters will do. seed_provider: # Addresses of hosts that are deemed contact points. # Cassandra nodes use this list of hosts to find each other and learn # the topology of the ring. You must change this if you are running # multiple nodes! - class_name: org.apache.cassandra.locator.SimpleSeedProvider parameters: # seeds is actually a comma-delimited list of addresses. # Ex: ",," - seeds: "172.17.0.2" # For workloads with more data than can fit in memory, Cassandra's # bottleneck will be reads that need to fetch data from # disk. "concurrent_reads" should be set to (16 * number_of_drives) in # order to allow the operations to enqueue low enough in the stack # that the OS and drives can reorder them. Same applies to # "concurrent_counter_writes", since counter writes read the current # values before incrementing and writing them back. # # On the other hand, since writes are almost never IO bound, the ideal # number of "concurrent_writes" is dependent on the number of cores in # your system; (8 * number_of_cores) is a good rule of thumb. concurrent_reads: 32 concurrent_writes: 32 concurrent_counter_writes: 32 # For materialized view writes, as there is a read involved, so this should # be limited by the less of concurrent reads or concurrent writes. concurrent_materialized_view_writes: 32 # Maximum memory to use for sstable chunk cache and buffer pooling. # 32MB of this are reserved for pooling buffers, the rest is used as a # cache that holds uncompressed sstable chunks. # Defaults to the smaller of 1/4 of heap or 512MB. This pool is allocated off-heap, # so is in addition to the memory allocated for heap. The cache also has on-heap # overhead which is roughly 128 bytes per chunk (i.e. 0.2% of the reserved size # if the default 64k chunk size is used). # Memory is only allocated when needed. # file_cache_size_in_mb: 512 # Flag indicating whether to allocate on or off heap when the sstable buffer # pool is exhausted, that is when it has exceeded the maximum memory # file_cache_size_in_mb, beyond which it will not cache buffers but allocate on request. # buffer_pool_use_heap_if_exhausted: true # The strategy for optimizing disk read # Possible values are: # ssd (for solid state disks, the default) # spinning (for spinning disks) # disk_optimization_strategy: ssd # Total permitted memory to use for memtables. Cassandra will stop # accepting writes when the limit is exceeded until a flush completes, # and will trigger a flush based on memtable_cleanup_threshold # If omitted, Cassandra will set both to 1/4 the size of the heap. # memtable_heap_space_in_mb: 2048 # memtable_offheap_space_in_mb: 2048 # memtable_cleanup_threshold is deprecated. The default calculation # is the only reasonable choice. See the comments on memtable_flush_writers # for more information. # # Ratio of occupied non-flushing memtable size to total permitted size # that will trigger a flush of the largest memtable. Larger mct will # mean larger flushes and hence less compaction, but also less concurrent # flush activity which can make it difficult to keep your disks fed # under heavy write load. # # memtable_cleanup_threshold defaults to 1 / (memtable_flush_writers + 1) # memtable_cleanup_threshold: 0.11 # Specify the way Cassandra allocates and manages memtable memory. # Options are: # # heap_buffers # on heap nio buffers # # offheap_buffers # off heap (direct) nio buffers # # offheap_objects # off heap objects memtable_allocation_type: heap_buffers # Total space to use for commit logs on disk. # # If space gets above this value, Cassandra will flush every dirty CF # in the oldest segment and remove it. So a small total commitlog space # will tend to cause more flush activity on less-active columnfamilies. # # The default value is the smaller of 8192, and 1/4 of the total space # of the commitlog volume. # # commitlog_total_space_in_mb: 8192 # This sets the number of memtable flush writer threads per disk # as well as the total number of memtables that can be flushed concurrently. # These are generally a combination of compute and IO bound. # # Memtable flushing is more CPU efficient than memtable ingest and a single thread # can keep up with the ingest rate of a whole server on a single fast disk # until it temporarily becomes IO bound under contention typically with compaction. # At that point you need multiple flush threads. At some point in the future # it may become CPU bound all the time. # # You can tell if flushing is falling behind using the MemtablePool.BlockedOnAllocation # metric which should be 0, but will be non-zero if threads are blocked waiting on flushing # to free memory. # # memtable_flush_writers defaults to two for a single data directory. # This means that two memtables can be flushed concurrently to the single data directory. # If you have multiple data directories the default is one memtable flushing at a time # but the flush will use a thread per data directory so you will get two or more writers. # # Two is generally enough to flush on a fast disk [array] mounted as a single data directory. # Adding more flush writers will result in smaller more frequent flushes that introduce more # compaction overhead. # # There is a direct tradeoff between number of memtables that can be flushed concurrently # and flush size and frequency. More is not better you just need enough flush writers # to never stall waiting for flushing to free memory. # #memtable_flush_writers: 2 # Total space to use for change-data-capture logs on disk. # # If space gets above this value, Cassandra will throw WriteTimeoutException # on Mutations including tables with CDC enabled. A CDCCompactor is responsible # for parsing the raw CDC logs and deleting them when parsing is completed. # # The default value is the min of 4096 mb and 1/8th of the total space # of the drive where cdc_raw_directory resides. # cdc_total_space_in_mb: 4096 # When we hit our cdc_raw limit and the CDCCompactor is either running behind # or experiencing backpressure, we check at the following interval to see if any # new space for cdc-tracked tables has been made available. Default to 250ms # cdc_free_space_check_interval_ms: 250 # A fixed memory pool size in MB for SSTable index summaries. If left # empty, this will default to 5% of the heap size. If the memory usage of # all index summaries exceeds this limit, SSTables with low read rates will # shrink their index summaries in order to meet this limit. However, this # is a best-effort process. In extreme conditions Cassandra may need to use # more than this amount of memory. index_summary_capacity_in_mb: # How frequently index summaries should be resampled. This is done # periodically to redistribute memory from the fixed-size pool to sstables # proportional their recent read rates. Setting to -1 will disable this # process, leaving existing index summaries at their current sampling level. index_summary_resize_interval_in_minutes: 60 # Whether to, when doing sequential writing, fsync() at intervals in # order to force the operating system to flush the dirty # buffers. Enable this to avoid sudden dirty buffer flushing from # impacting read latencies. Almost always a good idea on SSDs; not # necessarily on platters. trickle_fsync: false trickle_fsync_interval_in_kb: 10240 # TCP port, for commands and data # For security reasons, you should not expose this port to the internet. Firewall it if needed. storage_port: 7000 # SSL port, for encrypted communication. Unused unless enabled in # encryption_options # For security reasons, you should not expose this port to the internet. Firewall it if needed. ssl_storage_port: 7001 # Address or interface to bind to and tell other Cassandra nodes to connect to. # You _must_ change this if you want multiple nodes to be able to communicate! # # Set listen_address OR listen_interface, not both. # # Leaving it blank leaves it up to InetAddress.getLocalHost(). This # will always do the Right Thing _if_ the node is properly configured # (hostname, name resolution, etc), and the Right Thing is to use the # address associated with the hostname (it might not be). # # Setting listen_address to 0.0.0.0 is always wrong. # listen_address: 172.17.0.2 # Set listen_address OR listen_interface, not both. Interfaces must correspond # to a single address, IP aliasing is not supported. # listen_interface: eth0 # If you choose to specify the interface by name and the interface has an ipv4 and an ipv6 address # you can specify which should be chosen using listen_interface_prefer_ipv6. If false the first ipv4 # address will be used. If true the first ipv6 address will be used. Defaults to false preferring # ipv4. If there is only one address it will be selected regardless of ipv4/ipv6. # listen_interface_prefer_ipv6: false # Address to broadcast to other Cassandra nodes # Leaving this blank will set it to the same value as listen_address broadcast_address: 172.17.0.2 # When using multiple physical network interfaces, set this # to true to listen on broadcast_address in addition to # the listen_address, allowing nodes to communicate in both # interfaces. # Ignore this property if the network configuration automatically # routes between the public and private networks such as EC2. # listen_on_broadcast_address: false # Internode authentication backend, implementing IInternodeAuthenticator; # used to allow/disallow connections from peer nodes. # internode_authenticator: org.apache.cassandra.auth.AllowAllInternodeAuthenticator # Whether to start the native transport server. # Please note that the address on which the native transport is bound is the # same as the rpc_address. The port however is different and specified below. start_native_transport: true # port for the CQL native transport to listen for clients on # For security reasons, you should not expose this port to the internet. Firewall it if needed. native_transport_port: 9042 # Enabling native transport encryption in client_encryption_options allows you to either use # encryption for the standard port or to use a dedicated, additional port along with the unencrypted # standard native_transport_port. # Enabling client encryption and keeping native_transport_port_ssl disabled will use encryption # for native_transport_port. Setting native_transport_port_ssl to a different value # from native_transport_port will use encryption for native_transport_port_ssl while # keeping native_transport_port unencrypted. # native_transport_port_ssl: 9142 # The maximum threads for handling requests when the native transport is used. # This is similar to rpc_max_threads though the default differs slightly (and # there is no native_transport_min_threads, idle threads will always be stopped # after 30 seconds). # native_transport_max_threads: 128 # # The maximum size of allowed frame. Frame (requests) larger than this will # be rejected as invalid. The default is 256MB. If you're changing this parameter, # you may want to adjust max_value_size_in_mb accordingly. This should be positive and less than 2048. # native_transport_max_frame_size_in_mb: 256 # The maximum number of concurrent client connections. # The default is -1, which means unlimited. # native_transport_max_concurrent_connections: -1 # The maximum number of concurrent client connections per source ip. # The default is -1, which means unlimited. # native_transport_max_concurrent_connections_per_ip: -1 # Whether to start the thrift rpc server. start_rpc: false # The address or interface to bind the Thrift RPC service and native transport # server to. # # Set rpc_address OR rpc_interface, not both. # # Leaving rpc_address blank has the same effect as on listen_address # (i.e. it will be based on the configured hostname of the node). # # Note that unlike listen_address, you can specify 0.0.0.0, but you must also # set broadcast_rpc_address to a value other than 0.0.0.0. # # For security reasons, you should not expose this port to the internet. Firewall it if needed. rpc_address: 0.0.0.0 # Set rpc_address OR rpc_interface, not both. Interfaces must correspond # to a single address, IP aliasing is not supported. # rpc_interface: eth1 # If you choose to specify the interface by name and the interface has an ipv4 and an ipv6 address # you can specify which should be chosen using rpc_interface_prefer_ipv6. If false the first ipv4 # address will be used. If true the first ipv6 address will be used. Defaults to false preferring # ipv4. If there is only one address it will be selected regardless of ipv4/ipv6. # rpc_interface_prefer_ipv6: false # port for Thrift to listen for clients on rpc_port: 9160 # RPC address to broadcast to drivers and other Cassandra nodes. This cannot # be set to 0.0.0.0. If left blank, this will be set to the value of # rpc_address. If rpc_address is set to 0.0.0.0, broadcast_rpc_address must # be set. broadcast_rpc_address: 172.17.0.2 # enable or disable keepalive on rpc/native connections rpc_keepalive: true # Cassandra provides two out-of-the-box options for the RPC Server: # # sync # One thread per thrift connection. For a very large number of clients, memory # will be your limiting factor. On a 64 bit JVM, 180KB is the minimum stack size # per thread, and that will correspond to your use of virtual memory (but physical memory # may be limited depending on use of stack space). # # hsha # Stands for "half synchronous, half asynchronous." All thrift clients are handled # asynchronously using a small number of threads that does not vary with the amount # of thrift clients (and thus scales well to many clients). The rpc requests are still # synchronous (one thread per active request). If hsha is selected then it is essential # that rpc_max_threads is changed from the default value of unlimited. # # The default is sync because on Windows hsha is about 30% slower. On Linux, # sync/hsha performance is about the same, with hsha of course using less memory. # # Alternatively, can provide your own RPC server by providing the fully-qualified class name # of an o.a.c.t.TServerFactory that can create an instance of it. rpc_server_type: sync # Uncomment rpc_min|max_thread to set request pool size limits. # # Regardless of your choice of RPC server (see above), the number of maximum requests in the # RPC thread pool dictates how many concurrent requests are possible (but if you are using the sync # RPC server, it also dictates the number of clients that can be connected at all). # # The default is unlimited and thus provides no protection against clients overwhelming the server. You are # encouraged to set a maximum that makes sense for you in production, but do keep in mind that # rpc_max_threads represents the maximum number of client requests this server may execute concurrently. # # rpc_min_threads: 16 # rpc_max_threads: 2048 # uncomment to set socket buffer sizes on rpc connections # rpc_send_buff_size_in_bytes: # rpc_recv_buff_size_in_bytes: # Uncomment to set socket buffer size for internode communication # Note that when setting this, the buffer size is limited by net.core.wmem_max # and when not setting it it is defined by net.ipv4.tcp_wmem # See also: # /proc/sys/net/core/wmem_max # /proc/sys/net/core/rmem_max # /proc/sys/net/ipv4/tcp_wmem # /proc/sys/net/ipv4/tcp_wmem # and 'man tcp' # internode_send_buff_size_in_bytes: # Uncomment to set socket buffer size for internode communication # Note that when setting this, the buffer size is limited by net.core.wmem_max # and when not setting it it is defined by net.ipv4.tcp_wmem # internode_recv_buff_size_in_bytes: # Frame size for thrift (maximum message length). thrift_framed_transport_size_in_mb: 15 # Set to true to have Cassandra create a hard link to each sstable # flushed or streamed locally in a backups/ subdirectory of the # keyspace data. Removing these links is the operator's # responsibility. incremental_backups: false # Whether or not to take a snapshot before each compaction. Be # careful using this option, since Cassandra won't clean up the # snapshots for you. Mostly useful if you're paranoid when there # is a data format change. snapshot_before_compaction: false # Whether or not a snapshot is taken of the data before keyspace truncation # or dropping of column families. The STRONGLY advised default of true # should be used to provide data safety. If you set this flag to false, you will # lose data on truncation or drop. auto_snapshot: true # Granularity of the collation index of rows within a partition. # Increase if your rows are large, or if you have a very large # number of rows per partition. The competing goals are these: # # - a smaller granularity means more index entries are generated # and looking up rows within the partition by collation column # is faster # - but, Cassandra will keep the collation index in memory for hot # rows (as part of the key cache), so a larger granularity means # you can cache more hot rows column_index_size_in_kb: 64 # Per sstable indexed key cache entries (the collation index in memory # mentioned above) exceeding this size will not be held on heap. # This means that only partition information is held on heap and the # index entries are read from disk. # # Note that this size refers to the size of the # serialized index information and not the size of the partition. column_index_cache_size_in_kb: 2 # Number of simultaneous compactions to allow, NOT including # validation "compactions" for anti-entropy repair. Simultaneous # compactions can help preserve read performance in a mixed read/write # workload, by mitigating the tendency of small sstables to accumulate # during a single long running compactions. The default is usually # fine and if you experience problems with compaction running too # slowly or too fast, you should look at # compaction_throughput_mb_per_sec first. # # concurrent_compactors defaults to the smaller of (number of disks, # number of cores), with a minimum of 2 and a maximum of 8. # # If your data directories are backed by SSD, you should increase this # to the number of cores. #concurrent_compactors: 1 # Throttles compaction to the given total throughput across the entire # system. The faster you insert data, the faster you need to compact in # order to keep the sstable count down, but in general, setting this to # 16 to 32 times the rate you are inserting data is more than sufficient. # Setting this to 0 disables throttling. Note that this account for all types # of compaction, including validation compaction. compaction_throughput_mb_per_sec: 16 # When compacting, the replacement sstable(s) can be opened before they # are completely written, and used in place of the prior sstables for # any range that has been written. This helps to smoothly transfer reads # between the sstables, reducing page cache churn and keeping hot rows hot sstable_preemptive_open_interval_in_mb: 50 # Throttles all outbound streaming file transfers on this node to the # given total throughput in Mbps. This is necessary because Cassandra does # mostly sequential IO when streaming data during bootstrap or repair, which # can lead to saturating the network connection and degrading rpc performance. # When unset, the default is 200 Mbps or 25 MB/s. # stream_throughput_outbound_megabits_per_sec: 200 # Throttles all streaming file transfer between the datacenters, # this setting allows users to throttle inter dc stream throughput in addition # to throttling all network stream traffic as configured with # stream_throughput_outbound_megabits_per_sec # When unset, the default is 200 Mbps or 25 MB/s # inter_dc_stream_throughput_outbound_megabits_per_sec: 200 # How long the coordinator should wait for read operations to complete read_request_timeout_in_ms: 5000 # How long the coordinator should wait for seq or index scans to complete range_request_timeout_in_ms: 10000 # How long the coordinator should wait for writes to complete write_request_timeout_in_ms: 2000 # How long the coordinator should wait for counter writes to complete counter_write_request_timeout_in_ms: 5000 # How long a coordinator should continue to retry a CAS operation # that contends with other proposals for the same row cas_contention_timeout_in_ms: 1000 # How long the coordinator should wait for truncates to complete # (This can be much longer, because unless auto_snapshot is disabled # we need to flush first so we can snapshot before removing the data.) truncate_request_timeout_in_ms: 60000 # The default timeout for other, miscellaneous operations request_timeout_in_ms: 10000 # How long before a node logs slow queries. Select queries that take longer than # this timeout to execute, will generate an aggregated log message, so that slow queries # can be identified. Set this value to zero to disable slow query logging. slow_query_log_timeout_in_ms: 500 # Enable operation timeout information exchange between nodes to accurately # measure request timeouts. If disabled, replicas will assume that requests # were forwarded to them instantly by the coordinator, which means that # under overload conditions we will waste that much extra time processing # already-timed-out requests. # # Warning: before enabling this property make sure to ntp is installed # and the times are synchronized between the nodes. cross_node_timeout: false # Set keep-alive period for streaming # This node will send a keep-alive message periodically with this period. # If the node does not receive a keep-alive message from the peer for # 2 keep-alive cycles the stream session times out and fail # Default value is 300s (5 minutes), which means stalled stream # times out in 10 minutes by default # streaming_keep_alive_period_in_secs: 300 # phi value that must be reached for a host to be marked down. # most users should never need to adjust this. # phi_convict_threshold: 8 # endpoint_snitch -- Set this to a class that implements # IEndpointSnitch. The snitch has two functions: # # - it teaches Cassandra enough about your network topology to route # requests efficiently # - it allows Cassandra to spread replicas around your cluster to avoid # correlated failures. It does this by grouping machines into # "datacenters" and "racks." Cassandra will do its best not to have # more than one replica on the same "rack" (which may not actually # be a physical location) # # CASSANDRA WILL NOT ALLOW YOU TO SWITCH TO AN INCOMPATIBLE SNITCH # ONCE DATA IS INSERTED INTO THE CLUSTER. This would cause data loss. # This means that if you start with the default SimpleSnitch, which # locates every node on "rack1" in "datacenter1", your only options # if you need to add another datacenter are GossipingPropertyFileSnitch # (and the older PFS). From there, if you want to migrate to an # incompatible snitch like Ec2Snitch you can do it by adding new nodes # under Ec2Snitch (which will locate them in a new "datacenter") and # decommissioning the old ones. # # Out of the box, Cassandra provides: # # SimpleSnitch: # Treats Strategy order as proximity. This can improve cache # locality when disabling read repair. Only appropriate for # single-datacenter deployments. # # GossipingPropertyFileSnitch # This should be your go-to snitch for production use. The rack # and datacenter for the local node are defined in # cassandra-rackdc.properties and propagated to other nodes via # gossip. If cassandra-topology.properties exists, it is used as a # fallback, allowing migration from the PropertyFileSnitch. # # PropertyFileSnitch: # Proximity is determined by rack and data center, which are # explicitly configured in cassandra-topology.properties. # # Ec2Snitch: # Appropriate for EC2 deployments in a single Region. Loads Region # and Availability Zone information from the EC2 API. The Region is # treated as the datacenter, and the Availability Zone as the rack. # Only private IPs are used, so this will not work across multiple # Regions. # # Ec2MultiRegionSnitch: # Uses public IPs as broadcast_address to allow cross-region # connectivity. (Thus, you should set seed addresses to the public # IP as well.) You will need to open the storage_port or # ssl_storage_port on the public IP firewall. (For intra-Region # traffic, Cassandra will switch to the private IP after # establishing a connection.) # # RackInferringSnitch: # Proximity is determined by rack and data center, which are # assumed to correspond to the 3rd and 2nd octet of each node's IP # address, respectively. Unless this happens to match your # deployment conventions, this is best used as an example of # writing a custom Snitch class and is provided in that spirit. # # You can use a custom Snitch by setting this to the full class name # of the snitch, which will be assumed to be on your classpath. endpoint_snitch: SimpleSnitch # controls how often to perform the more expensive part of host score # calculation dynamic_snitch_update_interval_in_ms: 100 # controls how often to reset all host scores, allowing a bad host to # possibly recover dynamic_snitch_reset_interval_in_ms: 600000 # if set greater than zero and read_repair_chance is < 1.0, this will allow # 'pinning' of replicas to hosts in order to increase cache capacity. # The badness threshold will control how much worse the pinned host has to be # before the dynamic snitch will prefer other replicas over it. This is # expressed as a double which represents a percentage. Thus, a value of # 0.2 means Cassandra would continue to prefer the static snitch values # until the pinned host was 20% worse than the fastest. dynamic_snitch_badness_threshold: 0.1 # request_scheduler -- Set this to a class that implements # RequestScheduler, which will schedule incoming client requests # according to the specific policy. This is useful for multi-tenancy # with a single Cassandra cluster. # NOTE: This is specifically for requests from the client and does # not affect inter node communication. # org.apache.cassandra.scheduler.NoScheduler - No scheduling takes place # org.apache.cassandra.scheduler.RoundRobinScheduler - Round robin of # client requests to a node with a separate queue for each # request_scheduler_id. The scheduler is further customized by # request_scheduler_options as described below. request_scheduler: org.apache.cassandra.scheduler.NoScheduler # Scheduler Options vary based on the type of scheduler # # NoScheduler # Has no options # # RoundRobin # throttle_limit # The throttle_limit is the number of in-flight # requests per client. Requests beyond # that limit are queued up until # running requests can complete. # The value of 80 here is twice the number of # concurrent_reads + concurrent_writes. # default_weight # default_weight is optional and allows for # overriding the default which is 1. # weights # Weights are optional and will default to 1 or the # overridden default_weight. The weight translates into how # many requests are handled during each turn of the # RoundRobin, based on the scheduler id. # # request_scheduler_options: # throttle_limit: 80 # default_weight: 5 # weights: # Keyspace1: 1 # Keyspace2: 5 # request_scheduler_id -- An identifier based on which to perform # the request scheduling. Currently the only valid option is keyspace. # request_scheduler_id: keyspace # Enable or disable inter-node encryption # JVM defaults for supported SSL socket protocols and cipher suites can # be replaced using custom encryption options. This is not recommended # unless you have policies in place that dictate certain settings, or # need to disable vulnerable ciphers or protocols in case the JVM cannot # be updated. # FIPS compliant settings can be configured at JVM level and should not # involve changing encryption settings here: # https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/FIPS.html # *NOTE* No custom encryption options are enabled at the moment # The available internode options are : all, none, dc, rack # # If set to dc cassandra will encrypt the traffic between the DCs # If set to rack cassandra will encrypt the traffic between the racks # # The passwords used in these options must match the passwords used when generating # the keystore and truststore. For instructions on generating these files, see: # http://download.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html#CreateKeystore # server_encryption_options: internode_encryption: none keystore: conf/.keystore keystore_password: cassandra truststore: conf/.truststore truststore_password: cassandra # More advanced defaults below: # protocol: TLS # algorithm: SunX509 # store_type: JKS # cipher_suites: [TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA] # require_client_auth: false # require_endpoint_verification: false # enable or disable client/server encryption. client_encryption_options: enabled: false # If enabled and optional is set to true encrypted and unencrypted connections are handled. optional: false keystore: conf/.keystore keystore_password: cassandra # require_client_auth: false # Set trustore and truststore_password if require_client_auth is true # truststore: conf/.truststore # truststore_password: cassandra # More advanced defaults below: # protocol: TLS # algorithm: SunX509 # store_type: JKS # cipher_suites: [TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA] # internode_compression controls whether traffic between nodes is # compressed. # Can be: # # all # all traffic is compressed # # dc # traffic between different datacenters is compressed # # none # nothing is compressed. internode_compression: dc # Enable or disable tcp_nodelay for inter-dc communication. # Disabling it will result in larger (but fewer) network packets being sent, # reducing overhead from the TCP protocol itself, at the cost of increasing # latency if you block for cross-datacenter responses. inter_dc_tcp_nodelay: false # TTL for different trace types used during logging of the repair process. tracetype_query_ttl: 86400 tracetype_repair_ttl: 604800 # By default, Cassandra logs GC Pauses greater than 200 ms at INFO level # This threshold can be adjusted to minimize logging if necessary # gc_log_threshold_in_ms: 200 # If unset, all GC Pauses greater than gc_log_threshold_in_ms will log at # INFO level # UDFs (user defined functions) are disabled by default. # As of Cassandra 3.0 there is a sandbox in place that should prevent execution of evil code. enable_user_defined_functions: false # Enables scripted UDFs (JavaScript UDFs). # Java UDFs are always enabled, if enable_user_defined_functions is true. # Enable this option to be able to use UDFs with "language javascript" or any custom JSR-223 provider. # This option has no effect, if enable_user_defined_functions is false. enable_scripted_user_defined_functions: false # The default Windows kernel timer and scheduling resolution is 15.6ms for power conservation. # Lowering this value on Windows can provide much tighter latency and better throughput, however # some virtualized environments may see a negative performance impact from changing this setting # below their system default. The sysinternals 'clockres' tool can confirm your system's default # setting. windows_timer_interval: 1 # Enables encrypting data at-rest (on disk). Different key providers can be plugged in, but the default reads from # a JCE-style keystore. A single keystore can hold multiple keys, but the one referenced by # the "key_alias" is the only key that will be used for encrypt operations; previously used keys # can still (and should!) be in the keystore and will be used on decrypt operations # (to handle the case of key rotation). # # It is strongly recommended to download and install Java Cryptography Extension (JCE) # Unlimited Strength Jurisdiction Policy Files for your version of the JDK. # (current link: http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html) # # Currently, only the following file types are supported for transparent data encryption, although # more are coming in future cassandra releases: commitlog, hints transparent_data_encryption_options: enabled: false chunk_length_kb: 64 cipher: AES/CBC/PKCS5Padding key_alias: testing:1 # CBC IV length for AES needs to be 16 bytes (which is also the default size) # iv_length: 16 key_provider: - class_name: org.apache.cassandra.security.JKSKeyProvider parameters: - keystore: conf/.keystore keystore_password: cassandra store_type: JCEKS key_password: cassandra ##################### # SAFETY THRESHOLDS # ##################### # When executing a scan, within or across a partition, we need to keep the # tombstones seen in memory so we can return them to the coordinator, which # will use them to make sure other replicas also know about the deleted rows. # With workloads that generate a lot of tombstones, this can cause performance # problems and even exhaust the server heap. # (http://www.datastax.com/dev/blog/cassandra-anti-patterns-queues-and-queue-like-datasets) # Adjust the thresholds here if you understand the dangers and want to # scan more tombstones anyway. These thresholds may also be adjusted at runtime # using the StorageService mbean. tombstone_warn_threshold: 1000 tombstone_failure_threshold: 100000 # Log WARN on any multiple-partition batch size exceeding this value. 5kb per batch by default. # Caution should be taken on increasing the size of this threshold as it can lead to node instability. batch_size_warn_threshold_in_kb: 5 # Fail any multiple-partition batch exceeding this value. 50kb (10x warn threshold) by default. batch_size_fail_threshold_in_kb: 50 # Log WARN on any batches not of type LOGGED than span across more partitions than this limit unlogged_batch_across_partitions_warn_threshold: 10 # Log a warning when compacting partitions larger than this value compaction_large_partition_warning_threshold_mb: 100 # GC Pauses greater than gc_warn_threshold_in_ms will be logged at WARN level # Adjust the threshold based on your application throughput requirement # By default, Cassandra logs GC Pauses greater than 200 ms at INFO level gc_warn_threshold_in_ms: 1000 # Maximum size of any value in SSTables. Safety measure to detect SSTable corruption # early. Any value size larger than this threshold will result into marking an SSTable # as corrupted. This should be positive and less than 2048. # max_value_size_in_mb: 256 # Back-pressure settings # # If enabled, the coordinator will apply the back-pressure strategy specified below to each mutation # sent to replicas, with the aim of reducing pressure on overloaded replicas. back_pressure_enabled: false # The back-pressure strategy applied. # The default implementation, RateBasedBackPressure, takes three arguments: # high ratio, factor, and flow type, and uses the ratio between incoming mutation responses and outgoing mutation requests. # If below high ratio, outgoing mutations are rate limited according to the incoming rate decreased by the given factor; # if above high ratio, the rate limiting is increased by the given factor; # such factor is usually best configured between 1 and 10, use larger values for a faster recovery # at the expense of potentially more dropped mutations; # the rate limiting is applied according to the flow type: if FAST, it's rate limited at the speed of the fastest replica, # if SLOW at the speed of the slowest one. # New strategies can be added. Implementors need to implement org.apache.cassandra.net.BackpressureStrategy and # provide a public constructor accepting a Map. back_pressure_strategy: - class_name: org.apache.cassandra.net.RateBasedBackPressure parameters: - high_ratio: 0.90 factor: 5 flow: FAST # Coalescing Strategies # # Coalescing multiples messages turns out to significantly boost message processing throughput (think doubling or more). # On bare metal, the floor for packet processing throughput is high enough that many applications won't notice, but in # virtualized environments, the point at which an application can be bound by network packet processing can be # surprisingly low compared to the throughput of task processing that is possible inside a VM. It's not that bare metal # doesn't benefit from coalescing messages, it's that the number of packets a bare metal network interface can process # is sufficient for many applications such that no load starvation is experienced even without coalescing. # There are other benefits to coalescing network messages that are harder to isolate with a simple metric like messages # per second. By coalescing multiple tasks together, a network thread can process multiple messages for the cost of one # trip to read from a socket, and all the task submission work can be done at the same time reducing context switching # and increasing cache friendliness of network message processing. # See CASSANDRA-8692 for details. # Strategy to use for coalescing messages in OutboundTcpConnection. # Can be fixed, movingaverage, timehorizon, disabled (default). # You can also specify a subclass of CoalescingStrategies.CoalescingStrategy by name. # otc_coalescing_strategy: DISABLED # How many microseconds to wait for coalescing. For fixed strategy this is the amount of time after the first # message is received before it will be sent with any accompanying messages. For moving average this is the # maximum amount of time that will be waited as well as the interval at which messages must arrive on average # for coalescing to be enabled. # otc_coalescing_window_us: 200 # Do not try to coalesce messages if we already got that many messages. This should be more than 2 and less than 128. # otc_coalescing_enough_coalesced_messages: 8 # How many milliseconds to wait between two expiration runs on the backlog (queue) of the OutboundTcpConnection. # Expiration is done if messages are piling up in the backlog. Droppable messages are expired to free the memory # taken by expired messages. The interval should be between 0 and 1000, and in most installations the default value # will be appropriate. A smaller value could potentially expire messages slightly sooner at the expense of more CPU # time and queue contention while iterating the backlog of messages. # An interval of 0 disables any wait time, which is the behavior of former Cassandra versions. # # otc_backlog_expiration_interval_ms: 200 ================================================ FILE: modules/cassandra/src/test/resources/client-ssl/cassandra.cer.pem ================================================ Bag Attributes friendlyName: localhost localKeyID: 54 69 6D 65 20 31 37 32 39 33 34 38 39 36 38 31 31 39 subject=C = None, L = None, O = Testcontainers, OU = Testcontainers, CN = localhost issuer=C = None, L = None, O = Testcontainers, OU = Testcontainers, CN = localhost -----BEGIN CERTIFICATE----- MIIDbjCCAlagAwIBAgIJAKCVIipuH03/MA0GCSqGSIb3DQEBCwUAMGQxDTALBgNV BAYTBE5vbmUxDTALBgNVBAcTBE5vbmUxFzAVBgNVBAoTDlRlc3Rjb250YWluZXJz MRcwFQYDVQQLEw5UZXN0Y29udGFpbmVyczESMBAGA1UEAxMJbG9jYWxob3N0MCAX DTI0MTAxOTE0NDIwOFoYDzIxMjQwOTI1MTQ0MjA4WjBkMQ0wCwYDVQQGEwROb25l MQ0wCwYDVQQHEwROb25lMRcwFQYDVQQKEw5UZXN0Y29udGFpbmVyczEXMBUGA1UE CxMOVGVzdGNvbnRhaW5lcnMxEjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBALocrhrM1gYB/pF/qlDY+eFQZ9L8SMgCmn+I mgx/UbKqJwLp5wYuoW/PA4RwraFPkimf5CAE2kpBGcJu/Qzyp0fJZlBXpmkDJVrG pRbYz5mN4CrXNliYfAC1RzxvTT1tOjiDkk9kHVfs5nMVb9e2kq6tQEItflhlPzdD FOe0pY2XBX2stcQ6URRkK5buyPeLhnTrKMfLWEWKKKzSQGen+lbtBURZzkpmK88q qjLqqaZusXP6QlRVLqMADjQf7aXLi0A/fIhVrq1amqqiApJbijT0LP48DvS8DQQL jNKkQ17vMClMmXusU5IgJMlXfGEzeTNUI56wHGYUdE69FTGFvZECAwEAAaMhMB8w HQYDVR0OBBYEFNsvIE+IgkE0aTc+1MI7hpPQL2ZEMA0GCSqGSIb3DQEBCwUAA4IB AQC4U/tGPuRS3m/r1p3aAq0D88UGg6oKHwqe3re3xrFAv9y+Y3M+FXyh5w/yMCAr PcVo6Pef3hEjwc9wDuQoIcQ9eRZtYI1RnhkkuC8TZRk1KGKg9Lj4Zzbse7FfK92Z DUYgIVyhC/YkeEDwTiZI8WxhbglozNg5Ygw+qLK4rYmk+X/NgdfdQHocuJ3Jwqqx eYz0m2RUMhzxEI2z9jQr3DgjNkYrphLzaVXmO4MovzXx3DNeC8ADot9PGmaz24rl RDeSWynxbgqzdXGHxtyR0LY1k+Y+5wqU28L90D0o3ZtaMBnK+Ft2AP2zpbtgr8rR sf12uPyRUPzJQ46KNpjy4HN6 -----END CERTIFICATE----- ================================================ FILE: modules/cassandra/src/test/resources/client-ssl/cassandra.key.pem ================================================ Bag Attributes friendlyName: localhost localKeyID: 54 69 6D 65 20 31 37 32 39 33 34 38 39 36 38 31 31 39 Key Attributes: -----BEGIN PRIVATE KEY----- MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC6HK4azNYGAf6R f6pQ2PnhUGfS/EjIApp/iJoMf1GyqicC6ecGLqFvzwOEcK2hT5Ipn+QgBNpKQRnC bv0M8qdHyWZQV6ZpAyVaxqUW2M+ZjeAq1zZYmHwAtUc8b009bTo4g5JPZB1X7OZz FW/XtpKurUBCLX5YZT83QxTntKWNlwV9rLXEOlEUZCuW7sj3i4Z06yjHy1hFiiis 0kBnp/pW7QVEWc5KZivPKqoy6qmmbrFz+kJUVS6jAA40H+2ly4tAP3yIVa6tWpqq ogKSW4o09Cz+PA70vA0EC4zSpENe7zApTJl7rFOSICTJV3xhM3kzVCOesBxmFHRO vRUxhb2RAgMBAAECggEACqD5e7C+Rr8oz+jS/z8FAxsmcsqgFXW6NEjG4EPWx89a RWfthVFDov3XNsizzp/OulXWH2xnhkOyU7cm+Ia7JI+Z9w8Qz+dM5AkVA8Y23o9X TnSjNx57DODnEP21eZAzxpp50DlPFU02pzsbYhE2AbsFp0HirB9MI70CN24xR9hP i1zPgO7FVnLvn4INqVKgcV4vXlxvgDEvO4Myc1WoJXkyPCCObvEflBBWr2QwQfKT T2qjCJWv/P2PJGFaZbOrEvOHZjprSid4/n9gbQrodGoChhjiZT//l19Ay+7eJkUc yiiSK4u3fF4YPH9+CVpRQ94PHFe+0kQVvf+VGX/iqQKBgQDp5umTcZoho/KQzOd/ pA8fgnzbipEl5ep2MHB98cqEQ93eFv4l/bBehDQ1WvmFJY8SIzU4EQotpnCZd4Vb KH9PE8tsTRvw2cYbBuD751boLVaBn8wxtlTkFrN/CyAtV7w7AG0dXnDKushJx1NN 8AgvSr0X4hf0AKIWGtVteoX7qwKBgQDLse7Ze5dNbG48CpBGqQS4wFkTSEs5QKI5 68JXEQoCmJb06O7rxj4f5CALv3pReP3nrl5+kLmT8O+yU5C5dXf06k/z4GDJ6Esm 8XTEfB2Ca+kI2RLyMRRXPA2nEunbSsyk1AVo2GeRJxG4TaDbu2zTkUBAEyCuwarW OMsuYodPswKBgQCZc/kB1qH8OAdHoGawgv24+m7Xycz4RCLSb20d86edprjEn+kV G56+I5Xs+0aAZ+e5Sof7xJIc6Pkudg9zgtojEyV+ZAhUt0sVKCoqmdeWc0gxupjI dIq1KX+RdccieFDxlJIBlpgBKRGF9dNdaoC0JiBwrtBwMIomXmxvatbECQKBgQCS X1xZn/xLwJ0+PAENJauk72OS/aJAk/d/U7ElS7M7xlbDyxbVCnHeDNoSVxgYr68U 6zIwFOOmMb6tEGuxOX5n2nB1uUkUDf7jDyNvhhjWfaDJoOOCck5BmX/eDTNLR+bi kxEIFGnn3oFXRUFQZNCA/6GB6bzUl4qhwdIPlPHTDQKBgQCgztlUF5IOJFKMjVlY yoA/7+b5zwrh8Y2+SLzF/HLah85AuHxsgdTuQh+HLKSwJejKCT95BSRJO/kV0XCR KZGStqETpEH/2AJkxpjt0FZxtIQdnyTargbiipe4JzI3iCTLtfN5C9Pn3ZJ9giap B5uQm4762aH2jw1kKFegHlIgJg== -----END PRIVATE KEY----- ================================================ FILE: modules/cassandra/src/test/resources/initial-with-error.cql ================================================ CREATE KEYSPACE keySpaceTest WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 1}; USE keySpaceTest; /* The following statement contains an error (missing primary key) on purpose, do not fix it! */ CREATE TABLE catalog_category (id bigint); ================================================ FILE: modules/cassandra/src/test/resources/initial.cql ================================================ CREATE KEYSPACE keySpaceTest WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 1}; USE keySpaceTest; CREATE TABLE catalog_category (id bigint primary key, name text); INSERT INTO catalog_category (id, name) VALUES (1, 'test_category'); ================================================ FILE: modules/cassandra/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/chromadb/build.gradle ================================================ description = "Testcontainers :: ChromaDB" dependencies { api project(':testcontainers') testImplementation 'io.rest-assured:rest-assured:5.5.6' } ================================================ FILE: modules/chromadb/src/main/java/org/testcontainers/chromadb/ChromaDBContainer.java ================================================ package org.testcontainers.chromadb; import lombok.extern.slf4j.Slf4j; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.ComparableVersion; import org.testcontainers.utility.DockerImageName; /** * Testcontainers implementation of ChromaDB. *

* Supported images: {@code chromadb/chroma}, {@code ghcr.io/chroma-core/chroma} *

* Exposed ports: 8000 */ @Slf4j public class ChromaDBContainer extends GenericContainer { private static final DockerImageName DEFAULT_DOCKER_IMAGE = DockerImageName.parse("chromadb/chroma"); private static final DockerImageName GHCR_DOCKER_IMAGE = DockerImageName.parse("ghcr.io/chroma-core/chroma"); public ChromaDBContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public ChromaDBContainer(DockerImageName dockerImageName) { this(dockerImageName, isVersion2(dockerImageName.getVersionPart())); } public ChromaDBContainer(DockerImageName dockerImageName, boolean isVersion2) { super(dockerImageName); String apiPath = isVersion2 ? "/api/v2/heartbeat" : "/api/v1/heartbeat"; dockerImageName.assertCompatibleWith(DEFAULT_DOCKER_IMAGE, GHCR_DOCKER_IMAGE); withExposedPorts(8000); waitingFor(Wait.forHttp(apiPath)); } public String getEndpoint() { return "http://" + getHost() + ":" + getFirstMappedPort(); } private static boolean isVersion2(String version) { if (version.equals("latest")) { return true; } ComparableVersion comparableVersion = new ComparableVersion(version); if (comparableVersion.isGreaterThanOrEqualTo("1.0.0")) { return true; } log.warn("Version {} is less than 1.0.0 or not a semantic version.", version); return false; } } ================================================ FILE: modules/chromadb/src/test/java/org/testcontainers/chromadb/ChromaDBContainerTest.java ================================================ package org.testcontainers.chromadb; import io.restassured.http.ContentType; import org.junit.jupiter.api.Test; import static io.restassured.RestAssured.given; class ChromaDBContainerTest { @Test void test() { try ( // container { ChromaDBContainer chroma = new ChromaDBContainer("chromadb/chroma:0.4.23") // } ) { chroma.start(); given() .baseUri(chroma.getEndpoint()) .when() .body("{\"name\": \"test\"}") .contentType(ContentType.JSON) .post("/api/v1/databases") .then() .statusCode(200); given().baseUri(chroma.getEndpoint()).when().get("/api/v1/databases/test").then().statusCode(200); } } @Test void testVersion2() { try (ChromaDBContainer chroma = new ChromaDBContainer("chromadb/chroma:1.0.0")) { chroma.start(); given() .baseUri(chroma.getEndpoint()) .when() .body("{\"name\": \"test\"}") .contentType(ContentType.JSON) .post("/api/v2/tenants") .then() .statusCode(200); given().baseUri(chroma.getEndpoint()).when().get("/api/v2/tenants/test").then().statusCode(200); } } } ================================================ FILE: modules/chromadb/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/clickhouse/build.gradle ================================================ description = "Testcontainers :: JDBC :: ClickHouse" dependencies { api project(':testcontainers') api project(':testcontainers-jdbc') compileOnly project(':testcontainers-r2dbc') compileOnly(group: 'com.clickhouse', name: 'clickhouse-r2dbc', version: '0.9.4', classifier: 'http') testImplementation project(':testcontainers-jdbc-test') testRuntimeOnly(group: 'com.clickhouse', name: 'clickhouse-jdbc', version: '0.9.4', classifier: 'all') testImplementation 'com.clickhouse:client-v2:0.9.4' testImplementation testFixtures(project(':testcontainers-r2dbc')) testRuntimeOnly(group: 'com.clickhouse', name: 'clickhouse-r2dbc', version: '0.9.4', classifier: 'http') } ================================================ FILE: modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseContainer.java ================================================ package org.testcontainers.clickhouse; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import java.time.Duration; import java.util.HashSet; import java.util.Set; /** * Testcontainers implementation for ClickHouse. *

* Supported image: {@code clickhouse/clickhouse-server} *

* Exposed ports: *

    *
  • Database: 8123
  • *
  • Console: 9000
  • *
*/ public class ClickHouseContainer extends JdbcDatabaseContainer { static final String CLICKHOUSE_CLICKHOUSE_SERVER = "clickhouse/clickhouse-server"; private static final DockerImageName CLICKHOUSE_IMAGE_NAME = DockerImageName.parse(CLICKHOUSE_CLICKHOUSE_SERVER); static final Integer HTTP_PORT = 8123; static final Integer NATIVE_PORT = 9000; private static final String LEGACY_V1_DRIVER_CLASS_NAME = "com.clickhouse.jdbc.ClickHouseDriver"; private static final String DRIVER_CLASS_NAME = "com.clickhouse.jdbc.Driver"; private static final String JDBC_URL_PREFIX = "jdbc:clickhouse://"; private static final String TEST_QUERY = "SELECT 1"; static final String DEFAULT_USER = "test"; static final String DEFAULT_PASSWORD = "test"; private String databaseName = "default"; private String username = DEFAULT_USER; private String password = DEFAULT_PASSWORD; public ClickHouseContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public ClickHouseContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(CLICKHOUSE_IMAGE_NAME); addExposedPorts(HTTP_PORT, NATIVE_PORT); waitingFor( Wait .forHttp("/") .forPort(HTTP_PORT) .forStatusCode(200) .forResponsePredicate("Ok."::equals) .withStartupTimeout(Duration.ofMinutes(1)) ); } @Override protected void configure() { withEnv("CLICKHOUSE_DB", this.databaseName); withEnv("CLICKHOUSE_USER", this.username); withEnv("CLICKHOUSE_PASSWORD", this.password); } @Override public Set getLivenessCheckPortNumbers() { return new HashSet<>(getMappedPort(HTTP_PORT)); } @Override public String getDriverClassName() { try { Class.forName(DRIVER_CLASS_NAME); return DRIVER_CLASS_NAME; } catch (ClassNotFoundException e) { return LEGACY_V1_DRIVER_CLASS_NAME; } } @Override public String getJdbcUrl() { return ( JDBC_URL_PREFIX + getHost() + ":" + getMappedPort(HTTP_PORT) + "/" + this.databaseName + constructUrlParameters("?", "&") ); } public String getHttpUrl() { return "http://" + getHost() + ":" + getMappedPort(HTTP_PORT); } @Override public String getUsername() { return username; } @Override public String getPassword() { return password; } @Override public String getDatabaseName() { return databaseName; } @Override public String getTestQueryString() { return TEST_QUERY; } @Override public ClickHouseContainer withUsername(String username) { this.username = username; return this; } @Override public ClickHouseContainer withPassword(String password) { this.password = password; return this; } @Override public ClickHouseContainer withDatabaseName(String databaseName) { this.databaseName = databaseName; return this; } @Override protected void waitUntilContainerStarted() { getWaitStrategy().waitUntilReady(this); } } ================================================ FILE: modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainer.java ================================================ package org.testcontainers.clickhouse; import io.r2dbc.spi.ConnectionFactoryOptions; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; /** * ClickHouse R2DBC support */ public class ClickHouseR2DBCDatabaseContainer implements R2DBCDatabaseContainer { private final ClickHouseContainer container; public ClickHouseR2DBCDatabaseContainer(ClickHouseContainer container) { this.container = container; } public static ConnectionFactoryOptions getOptions(ClickHouseContainer container) { ConnectionFactoryOptions options = ConnectionFactoryOptions .builder() .option(ConnectionFactoryOptions.DRIVER, ClickHouseR2DBCDatabaseContainerProvider.DRIVER) .build(); return new ClickHouseR2DBCDatabaseContainer(container).configure(options); } @Override public void start() { this.container.start(); } @Override public void stop() { this.container.stop(); } @Override public ConnectionFactoryOptions configure(ConnectionFactoryOptions options) { return options .mutate() .option(ConnectionFactoryOptions.HOST, container.getHost()) .option(ConnectionFactoryOptions.PORT, container.getMappedPort(ClickHouseContainer.HTTP_PORT)) .option(ConnectionFactoryOptions.DATABASE, container.getDatabaseName()) .option(ConnectionFactoryOptions.USER, container.getUsername()) .option(ConnectionFactoryOptions.PASSWORD, container.getPassword()) .option(ConnectionFactoryOptions.PROTOCOL, "http") .build(); } } ================================================ FILE: modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainerProvider.java ================================================ package org.testcontainers.clickhouse; import com.clickhouse.r2dbc.connection.ClickHouseConnectionFactoryProvider; import io.r2dbc.spi.ConnectionFactoryMetadata; import io.r2dbc.spi.ConnectionFactoryOptions; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; import org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider; import javax.annotation.Nullable; public class ClickHouseR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider { static final String DRIVER = ClickHouseConnectionFactoryProvider.CLICKHOUSE_DRIVER; @Override public boolean supports(ConnectionFactoryOptions options) { return DRIVER.equals(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)); } @Override public R2DBCDatabaseContainer createContainer(ConnectionFactoryOptions options) { String image = ClickHouseContainer.CLICKHOUSE_CLICKHOUSE_SERVER + ":" + options.getRequiredValue(IMAGE_TAG_OPTION); ClickHouseContainer container = new ClickHouseContainer(image) .withDatabaseName((String) options.getRequiredValue(ConnectionFactoryOptions.DATABASE)); if (Boolean.TRUE.equals(options.getValue(REUSABLE_OPTION))) { container.withReuse(true); } return new ClickHouseR2DBCDatabaseContainer(container); } @Nullable @Override public ConnectionFactoryMetadata getMetadata(ConnectionFactoryOptions options) { ConnectionFactoryOptions.Builder builder = options.mutate(); if (!options.hasOption(ConnectionFactoryOptions.USER)) { builder.option(ConnectionFactoryOptions.USER, ClickHouseContainer.DEFAULT_USER); } if (!options.hasOption(ConnectionFactoryOptions.PASSWORD)) { builder.option(ConnectionFactoryOptions.PASSWORD, ClickHouseContainer.DEFAULT_PASSWORD); } builder.option(ConnectionFactoryOptions.PROTOCOL, "http"); return R2DBCDatabaseContainerProvider.super.getMetadata(builder.build()); } } ================================================ FILE: modules/clickhouse/src/main/java/org/testcontainers/containers/ClickHouseContainer.java ================================================ package org.testcontainers.containers; import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; import org.testcontainers.utility.ComparableVersion; import org.testcontainers.utility.DockerImageName; import java.time.Duration; import java.util.HashSet; import java.util.Set; /** * Testcontainers implementation for ClickHouse. * * @deprecated use {@link org.testcontainers.clickhouse.ClickHouseContainer} instead */ public class ClickHouseContainer extends JdbcDatabaseContainer { public static final String NAME = "clickhouse"; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("yandex/clickhouse-server"); private static final DockerImageName CLICKHOUSE_IMAGE_NAME = DockerImageName.parse("clickhouse/clickhouse-server"); @Deprecated public static final String IMAGE = DEFAULT_IMAGE_NAME.getUnversionedPart(); @Deprecated public static final String DEFAULT_TAG = "18.10.3"; public static final Integer HTTP_PORT = 8123; public static final Integer NATIVE_PORT = 9000; private static final String LEGACY_DRIVER_CLASS_NAME = "ru.yandex.clickhouse.ClickHouseDriver"; private static final String DRIVER_CLASS_NAME = "com.clickhouse.jdbc.ClickHouseDriver"; private static final String JDBC_URL_PREFIX = "jdbc:" + NAME + "://"; private static final String TEST_QUERY = "SELECT 1"; private String databaseName = "default"; private String username = "default"; private String password = ""; private boolean supportsNewDriver; /** * @deprecated use {@link #ClickHouseContainer(DockerImageName)} instead */ @Deprecated public ClickHouseContainer() { this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } public ClickHouseContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public ClickHouseContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, CLICKHOUSE_IMAGE_NAME); supportsNewDriver = isNewDriverSupported(dockerImageName); addExposedPorts(HTTP_PORT, NATIVE_PORT); this.waitStrategy = new HttpWaitStrategy() .forStatusCode(200) .forResponsePredicate("Ok."::equals) .withStartupTimeout(Duration.ofMinutes(1)); } @Override public Set getLivenessCheckPortNumbers() { return new HashSet<>(getMappedPort(HTTP_PORT)); } @Override public String getDriverClassName() { try { if (supportsNewDriver) { Class.forName(DRIVER_CLASS_NAME); return DRIVER_CLASS_NAME; } else { return LEGACY_DRIVER_CLASS_NAME; } } catch (ClassNotFoundException e) { return LEGACY_DRIVER_CLASS_NAME; } } private static boolean isNewDriverSupported(DockerImageName dockerImageName) { // New driver supports versions 20.7+. Check the version part of the tag return new ComparableVersion(dockerImageName.getVersionPart()).isGreaterThanOrEqualTo("20.7"); } @Override public String getJdbcUrl() { return JDBC_URL_PREFIX + getHost() + ":" + getMappedPort(HTTP_PORT) + "/" + databaseName; } @Override public String getUsername() { return username; } @Override public String getPassword() { return password; } @Override public String getTestQueryString() { return TEST_QUERY; } @Override public ClickHouseContainer withUrlParam(String paramName, String paramValue) { throw new UnsupportedOperationException("The ClickHouse does not support this"); } } ================================================ FILE: modules/clickhouse/src/main/java/org/testcontainers/containers/ClickHouseProvider.java ================================================ package org.testcontainers.containers; import org.testcontainers.clickhouse.ClickHouseContainer; import org.testcontainers.utility.DockerImageName; public class ClickHouseProvider extends JdbcDatabaseContainerProvider { private static final String DEFAULT_TAG = "24.12-alpine"; @Override public boolean supports(String databaseType) { return databaseType.equals("clickhouse"); } @Override public JdbcDatabaseContainer newInstance() { return newInstance(DEFAULT_TAG); } @Override public JdbcDatabaseContainer newInstance(String tag) { return new ClickHouseContainer(DockerImageName.parse("clickhouse/clickhouse-server").withTag(tag)); } } ================================================ FILE: modules/clickhouse/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider ================================================ org.testcontainers.containers.ClickHouseProvider ================================================ FILE: modules/clickhouse/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider ================================================ org.testcontainers.clickhouse.ClickHouseR2DBCDatabaseContainerProvider ================================================ FILE: modules/clickhouse/src/test/java/org/testcontainers/ClickhouseTestImages.java ================================================ package org.testcontainers; import org.testcontainers.utility.DockerImageName; public interface ClickhouseTestImages { DockerImageName CLICKHOUSE_IMAGE = DockerImageName.parse("clickhouse/clickhouse-server:21.11.11-alpine"); DockerImageName CLICKHOUSE_24_12_IMAGE = DockerImageName.parse("clickhouse/clickhouse-server:24.12-alpine"); } ================================================ FILE: modules/clickhouse/src/test/java/org/testcontainers/clickhouse/ClickHouseContainerTest.java ================================================ package org.testcontainers.clickhouse; import com.clickhouse.client.api.Client; import com.clickhouse.client.api.data_formats.ClickHouseBinaryFormatReader; import com.clickhouse.client.api.query.QueryResponse; import org.junit.jupiter.api.Test; import org.testcontainers.ClickhouseTestImages; import org.testcontainers.db.AbstractContainerDatabaseTest; import java.sql.ResultSet; import java.sql.SQLException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; class ClickHouseContainerTest extends AbstractContainerDatabaseTest { @Test void testSimple() throws SQLException { try ( // container { ClickHouseContainer clickhouse = new ClickHouseContainer("clickhouse/clickhouse-server:21.11-alpine") // } ) { clickhouse.start(); ResultSet resultSet = performQuery(clickhouse, "SELECT 1"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).isEqualTo(1); } } @Test void customCredentialsWithUrlParams() throws SQLException { try ( ClickHouseContainer clickhouse = new ClickHouseContainer("clickhouse/clickhouse-server:21.11.2-alpine") .withUsername("default") .withPassword("") .withDatabaseName("test") // The new driver uses the prefix `clickhouse_setting_` for session settings .withUrlParam("clickhouse_setting_max_result_rows", "5") ) { clickhouse.start(); ResultSet resultSet = performQuery( clickhouse, "SELECT value FROM system.settings where name='max_result_rows'" ); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).isEqualTo(5); } } @Test void testNewAuth() throws SQLException { try (ClickHouseContainer clickhouse = new ClickHouseContainer(ClickhouseTestImages.CLICKHOUSE_24_12_IMAGE)) { clickhouse.start(); ResultSet resultSet = performQuery(clickhouse, "SELECT 1"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).isEqualTo(1); } } @Test void testGetHttpMethodWithHttpClient() { ClickHouseContainer clickhouse = new ClickHouseContainer(ClickhouseTestImages.CLICKHOUSE_24_12_IMAGE); clickhouse.start(); Client client = new Client.Builder() .addEndpoint(clickhouse.getHttpUrl()) .setUsername(clickhouse.getUsername()) .setPassword(clickhouse.getPassword()) .build(); try { QueryResponse queryResponse = client.query("SELECT 1").get(1, TimeUnit.MINUTES); ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(queryResponse); reader.next(); int result = reader.getInteger(1); assertThat(result).isEqualTo(1); } catch (ExecutionException | InterruptedException | TimeoutException e) { fail("Cannot get sql result:" + e); } finally { clickhouse.close(); client.close(); } } } ================================================ FILE: modules/clickhouse/src/test/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainerTest.java ================================================ package org.testcontainers.clickhouse; import io.r2dbc.spi.ConnectionFactoryOptions; import org.testcontainers.r2dbc.AbstractR2DBCDatabaseContainerTest; public class ClickHouseR2DBCDatabaseContainerTest extends AbstractR2DBCDatabaseContainerTest { @Override protected ConnectionFactoryOptions getOptions(ClickHouseContainer container) { return ClickHouseR2DBCDatabaseContainer.getOptions(container); } @Override protected String createR2DBCUrl() { return "r2dbc:tc:clickhouse:///db?TC_IMAGE_TAG=21.11.11-alpine"; } @Override protected ClickHouseContainer createContainer() { return new ClickHouseContainer("clickhouse/clickhouse-server:21.11.11-alpine"); } } ================================================ FILE: modules/clickhouse/src/test/java/org/testcontainers/jdbc/clickhouse/ClickhouseJDBCDriverTest.java ================================================ package org.testcontainers.jdbc.clickhouse; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; class ClickhouseJDBCDriverTest extends AbstractJDBCDriverTest { public static Iterable data() { return Arrays.asList( new Object[][] { // { "jdbc:tc:clickhouse://hostname/databasename", EnumSet.of(Options.PmdKnownBroken) }, } ); } } ================================================ FILE: modules/clickhouse/src/test/java/org/testcontainers/junit/clickhouse/SimpleClickhouseTest.java ================================================ package org.testcontainers.junit.clickhouse; import org.junit.jupiter.api.Test; import org.testcontainers.ClickhouseTestImages; import org.testcontainers.containers.ClickHouseContainer; import org.testcontainers.db.AbstractContainerDatabaseTest; import java.sql.ResultSet; import java.sql.SQLException; import static org.assertj.core.api.Assertions.assertThat; class SimpleClickhouseTest extends AbstractContainerDatabaseTest { @Test public void testSimple() throws SQLException { try (ClickHouseContainer clickhouse = new ClickHouseContainer(ClickhouseTestImages.CLICKHOUSE_IMAGE)) { clickhouse.start(); ResultSet resultSet = performQuery(clickhouse, "SELECT 1"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); } } } ================================================ FILE: modules/clickhouse/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/cockroachdb/build.gradle ================================================ description = "Testcontainers :: JDBC :: CockroachDB" dependencies { api project(':testcontainers-jdbc') testRuntimeOnly 'org.postgresql:postgresql:42.7.8' testImplementation project(':testcontainers-jdbc-test') } ================================================ FILE: modules/cockroachdb/src/main/java/org/testcontainers/cockroachdb/CockroachContainer.java ================================================ package org.testcontainers.cockroachdb; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.containers.wait.strategy.WaitAllStrategy; import org.testcontainers.utility.ComparableVersion; import org.testcontainers.utility.DockerImageName; import java.time.Duration; /** * Testcontainers implementation for CockroachDB. *

* Supported image: {@code cockroachdb/cockroach} *

* Exposed ports: *

    *
  • Database: 26257
  • *
  • Console: 8080
  • *
*/ public class CockroachContainer extends JdbcDatabaseContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("cockroachdb/cockroach"); public static final String NAME = "cockroach"; private static final String JDBC_DRIVER_CLASS_NAME = "org.postgresql.Driver"; private static final String JDBC_URL_PREFIX = "jdbc:postgresql"; private static final String TEST_QUERY_STRING = "SELECT 1"; private static final int REST_API_PORT = 8080; private static final int DB_PORT = 26257; private static final String FIRST_VERSION_WITH_ENV_VARS_SUPPORT = "22.1.0"; private String databaseName = "postgres"; private String username = "root"; private String password = ""; private boolean isVersionGreaterThanOrEqualTo221; public CockroachContainer(final String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public CockroachContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); this.isVersionGreaterThanOrEqualTo221 = isVersionGreaterThanOrEqualTo221(dockerImageName); 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; } @Override public String getJdbcUrl() { String additionalUrlParams = constructUrlParameters("?", "&"); return ( JDBC_URL_PREFIX + "://" + getHost() + ":" + getMappedPort(DB_PORT) + "/" + databaseName + additionalUrlParams ); } @Override public final String getDatabaseName() { return databaseName; } @Override public String getUsername() { return username; } @Override public String getPassword() { return password; } @Override public String getTestQueryString() { return TEST_QUERY_STRING; } @Override public CockroachContainer withUsername(String username) { validateIfVersionSupportsUsernameOrPasswordOrDatabase("username"); this.username = username; return this; } @Override public CockroachContainer withPassword(String password) { validateIfVersionSupportsUsernameOrPasswordOrDatabase("password"); this.password = password; return this; } @Override public CockroachContainer withDatabaseName(final String databaseName) { validateIfVersionSupportsUsernameOrPasswordOrDatabase("databaseName"); this.databaseName = databaseName; return this; } private boolean isVersionGreaterThanOrEqualTo221(DockerImageName dockerImageName) { ComparableVersion version = new ComparableVersion(dockerImageName.getVersionPart().replaceFirst("v", "")); return version.isGreaterThanOrEqualTo(FIRST_VERSION_WITH_ENV_VARS_SUPPORT); } private void validateIfVersionSupportsUsernameOrPasswordOrDatabase(String parameter) { if (!isVersionGreaterThanOrEqualTo221) { throw new UnsupportedOperationException( String.format("Setting a %s in not supported in the versions below 22.1.0", parameter) ); } } @Override protected void waitUntilContainerStarted() { getWaitStrategy().waitUntilReady(this); } } ================================================ FILE: modules/cockroachdb/src/main/java/org/testcontainers/containers/CockroachContainer.java ================================================ package org.testcontainers.containers; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.containers.wait.strategy.WaitAllStrategy; import org.testcontainers.utility.ComparableVersion; import org.testcontainers.utility.DockerImageName; import java.time.Duration; /** * Testcontainers implementation for CockroachDB. *

* Supported image: {@code cockroachdb/cockroach} *

* Exposed ports: *

    *
  • Database: 26257
  • *
  • Console: 8080
  • *
* * @deprecated use {@link org.testcontainers.cockroachdb.CockroachContainer} instead */ @Deprecated public class CockroachContainer extends JdbcDatabaseContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("cockroachdb/cockroach"); private static final String DEFAULT_TAG = "v19.2.11"; public static final String NAME = "cockroach"; @Deprecated public static final String IMAGE = DEFAULT_IMAGE_NAME.getUnversionedPart(); @Deprecated public static final String IMAGE_TAG = DEFAULT_TAG; private static final String JDBC_DRIVER_CLASS_NAME = "org.postgresql.Driver"; private static final String JDBC_URL_PREFIX = "jdbc:postgresql"; private static final String TEST_QUERY_STRING = "SELECT 1"; private static final int REST_API_PORT = 8080; private static final int DB_PORT = 26257; private static final String FIRST_VERSION_WITH_ENV_VARS_SUPPORT = "22.1.0"; private String databaseName = "postgres"; private String username = "root"; private String password = ""; private boolean isVersionGreaterThanOrEqualTo221; /** * @deprecated use {@link #CockroachContainer(DockerImageName)} instead */ @Deprecated public CockroachContainer() { this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } public CockroachContainer(final String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public CockroachContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); this.isVersionGreaterThanOrEqualTo221 = isVersionGreaterThanOrEqualTo221(dockerImageName); 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; } @Override public String getJdbcUrl() { String additionalUrlParams = constructUrlParameters("?", "&"); return ( JDBC_URL_PREFIX + "://" + getHost() + ":" + getMappedPort(DB_PORT) + "/" + databaseName + additionalUrlParams ); } @Override public final String getDatabaseName() { return databaseName; } @Override public String getUsername() { return username; } @Override public String getPassword() { return password; } @Override public String getTestQueryString() { return TEST_QUERY_STRING; } @Override public CockroachContainer withUsername(String username) { validateIfVersionSupportsUsernameOrPasswordOrDatabase("username"); this.username = username; return this; } @Override public CockroachContainer withPassword(String password) { validateIfVersionSupportsUsernameOrPasswordOrDatabase("password"); this.password = password; return this; } @Override public CockroachContainer withDatabaseName(final String databaseName) { validateIfVersionSupportsUsernameOrPasswordOrDatabase("databaseName"); this.databaseName = databaseName; return this; } private boolean isVersionGreaterThanOrEqualTo221(DockerImageName dockerImageName) { ComparableVersion version = new ComparableVersion(dockerImageName.getVersionPart().replaceFirst("v", "")); return version.isGreaterThanOrEqualTo(FIRST_VERSION_WITH_ENV_VARS_SUPPORT); } private void validateIfVersionSupportsUsernameOrPasswordOrDatabase(String parameter) { if (!isVersionGreaterThanOrEqualTo221) { throw new UnsupportedOperationException( String.format("Setting a %s in not supported in the versions below 22.1.0", parameter) ); } } @Override protected void waitUntilContainerStarted() { getWaitStrategy().waitUntilReady(this); } } ================================================ FILE: modules/cockroachdb/src/main/java/org/testcontainers/containers/CockroachContainerProvider.java ================================================ package org.testcontainers.containers; import org.testcontainers.utility.DockerImageName; public class CockroachContainerProvider extends JdbcDatabaseContainerProvider { @Override public boolean supports(String databaseType) { return databaseType.equals(CockroachContainer.NAME); } @Override public JdbcDatabaseContainer newInstance() { return newInstance(CockroachContainer.IMAGE_TAG); } @Override public JdbcDatabaseContainer newInstance(String tag) { return new CockroachContainer(DockerImageName.parse(CockroachContainer.IMAGE).withTag(tag)); } } ================================================ FILE: modules/cockroachdb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider ================================================ org.testcontainers.containers.CockroachContainerProvider ================================================ FILE: modules/cockroachdb/src/test/java/org/testcontainers/CockroachDBTestImages.java ================================================ package org.testcontainers; import org.testcontainers.utility.DockerImageName; public interface CockroachDBTestImages { DockerImageName COCKROACHDB_IMAGE = DockerImageName.parse("cockroachdb/cockroach:v22.2.3"); } ================================================ FILE: modules/cockroachdb/src/test/java/org/testcontainers/cockroachdb/CockroachContainerTest.java ================================================ package org.testcontainers.cockroachdb; import org.junit.jupiter.api.Test; import org.testcontainers.CockroachDBTestImages; import org.testcontainers.db.AbstractContainerDatabaseTest; import org.testcontainers.images.builder.Transferable; import java.sql.ResultSet; import java.sql.SQLException; import java.util.logging.Level; import java.util.logging.LogManager; import static org.assertj.core.api.Assertions.assertThat; class CockroachContainerTest extends AbstractContainerDatabaseTest { static { // Postgres JDBC driver uses JUL; disable it to avoid annoying, irrelevant, stderr logs during connection testing LogManager.getLogManager().getLogger("").setLevel(Level.OFF); } @Test void testSimple() throws SQLException { try ( // container { CockroachContainer cockroach = new CockroachContainer("cockroachdb/cockroach:v26.1.1") // } ) { cockroach.start(); ResultSet resultSet = performQuery(cockroach, "SELECT 1"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); } } @Test void testExplicitInitScript() throws SQLException { try ( CockroachContainer cockroach = new CockroachContainer(CockroachDBTestImages.COCKROACHDB_IMAGE) .withInitScript("somepath/init_postgresql.sql") ) { // 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"); } } @Test void testWithAdditionalUrlParamInJdbcUrl() { CockroachContainer cockroach = new CockroachContainer(CockroachDBTestImages.COCKROACHDB_IMAGE) .withUrlParam("sslmode", "disable") .withUrlParam("application_name", "cockroach"); try { cockroach.start(); String jdbcUrl = cockroach.getJdbcUrl(); assertThat(jdbcUrl) .contains("?") .contains("&") .contains("sslmode=disable") .contains("application_name=cockroach"); } finally { cockroach.stop(); } } @Test void testWithUsernamePasswordDatabase() throws SQLException { try ( CockroachContainer cockroach = new CockroachContainer(CockroachDBTestImages.COCKROACHDB_IMAGE) .withUsername("test_user") .withPassword("test_password") .withDatabaseName("test_database") ) { cockroach.start(); ResultSet resultSet = performQuery(cockroach, "SELECT 1"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); String jdbcUrl = cockroach.getJdbcUrl(); assertThat(jdbcUrl).contains("/" + "test_database"); } } @Test 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"); } } } ================================================ FILE: modules/cockroachdb/src/test/java/org/testcontainers/jdbc/cockroachdb/CockroachDBJDBCDriverTest.java ================================================ package org.testcontainers.jdbc.cockroachdb; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; class CockroachDBJDBCDriverTest extends AbstractJDBCDriverTest { public static Iterable data() { return Arrays.asList( new Object[][] { // { "jdbc:tc:cockroach:v22.2.3://hostname/databasename", EnumSet.noneOf(Options.class) }, } ); } } ================================================ FILE: modules/cockroachdb/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/cockroachdb/src/test/resources/somepath/init_postgresql.sql ================================================ CREATE TABLE bar ( foo VARCHAR(255) ); INSERT INTO bar (foo) VALUES ('hello world'); ================================================ FILE: modules/consul/build.gradle ================================================ description = "Testcontainers :: Consul" dependencies { api project(':testcontainers') testImplementation 'com.ecwid.consul:consul-api:1.4.5' testImplementation 'io.rest-assured:rest-assured:5.5.6' } ================================================ FILE: modules/consul/src/main/java/org/testcontainers/consul/ConsulContainer.java ================================================ package org.testcontainers.consul; import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.api.model.Capability; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; /** * Testcontainers implementation for Consul. *

* Supported images: {@code hashicorp/consul}, {@code consul} *

* Exposed ports: *

    *
  • HTTP: 8500
  • *
  • gRPC: 8502
  • *
*/ public class ConsulContainer extends GenericContainer { private static final DockerImageName DEFAULT_OLD_IMAGE_NAME = DockerImageName.parse("consul"); private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("hashicorp/consul"); private static final int CONSUL_HTTP_PORT = 8500; private static final int CONSUL_GRPC_PORT = 8502; private List initCommands = new ArrayList<>(); private String[] startConsulCmd = new String[] { "agent", "-dev", "-client", "0.0.0.0" }; public ConsulContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public ConsulContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_OLD_IMAGE_NAME, DEFAULT_IMAGE_NAME); // Use the status leader endpoint to verify if consul is running. setWaitStrategy(Wait.forHttp("/v1/status/leader").forPort(CONSUL_HTTP_PORT).forStatusCode(200)); withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withCapAdd(Capability.IPC_LOCK)); withEnv("CONSUL_ADDR", "http://0.0.0.0:" + CONSUL_HTTP_PORT); withCommand(startConsulCmd); withExposedPorts(CONSUL_HTTP_PORT, CONSUL_GRPC_PORT); } @Override protected void containerIsStarted(InspectContainerResponse containerInfo) { runConsulCommands(); } private void runConsulCommands() { if (!initCommands.isEmpty()) { String commands = initCommands .stream() .map(command -> "consul " + command) .collect(Collectors.joining(" && ")); try { ExecResult execResult = this.execInContainer(new String[] { "/bin/sh", "-c", commands }); if (execResult.getExitCode() != 0) { logger() .error( "Failed to execute these init commands {}. Exit code {}. Stdout {}. Stderr {}", initCommands, execResult.getExitCode(), execResult.getStdout(), execResult.getStderr() ); } } catch (IOException | InterruptedException e) { logger() .error( "Failed to execute these init commands {}. Exception message: {}", initCommands, e.getMessage() ); } } } /** * Run consul commands using the consul cli. * * Useful for enabling more secret engines like: *
     *     .withConsulCommand("secrets enable pki")
     *     .withConsulCommand("secrets enable transit")
     * 
* or register specific K/V like: *
     *     .withConsulCommand("kv put config/testing1 value123")
     * 
* @param commands The commands to send to the consul cli * @return this */ public ConsulContainer withConsulCommand(String... commands) { initCommands.addAll(Arrays.asList(commands)); return self(); } } ================================================ FILE: modules/consul/src/test/java/org/testcontainers/consul/ConsulContainerTest.java ================================================ package org.testcontainers.consul; import com.ecwid.consul.v1.ConsulClient; import com.ecwid.consul.v1.Response; import com.ecwid.consul.v1.kv.model.GetValue; import io.restassured.RestAssured; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.HashMap; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; class ConsulContainerTest { private static ConsulContainer consul = new ConsulContainer("hashicorp/consul:1.15") .withConsulCommand("kv put config/testing1 value123"); @BeforeAll static void setup() { consul.start(); } @AfterAll static void teardown() { consul.stop(); } @Test void readFirstPropertyPathWithCli() throws IOException, InterruptedException { GenericContainer.ExecResult result = consul.execInContainer("consul", "kv", "get", "config/testing1"); final String output = result.getStdout().replaceAll("\\r?\\n", ""); assertThat(output).contains("value123"); } @Test void readFirstSecretPathOverHttpApi() { io.restassured.response.Response response = RestAssured .given() .when() .get("http://" + getHostAndPort() + "/v1/kv/config/testing1") .andReturn(); assertThat(response.body().jsonPath().getString("[0].Value")) .isEqualTo(Base64.getEncoder().encodeToString("value123".getBytes(StandardCharsets.UTF_8))); } @Test void writeAndReadMultipleValuesUsingClient() { final ConsulClient consulClient = new ConsulClient(consul.getHost(), consul.getFirstMappedPort()); final Map properties = new HashMap<>(); properties.put("value", "world"); properties.put("other_value", "another world"); // Write operation properties.forEach((key, value) -> { Response writeResponse = consulClient.setKVValue(key, value); assertThat(writeResponse.getValue()).isTrue(); }); // Read operation properties.forEach((key, value) -> { Response readResponse = consulClient.getKVValue(key); assertThat(readResponse.getValue().getDecodedValue()).isEqualTo(value); }); } private String getHostAndPort() { return consul.getHost() + ":" + consul.getMappedPort(8500); } } ================================================ FILE: modules/consul/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/couchbase/AUTHORS ================================================ Tayeb Chlyah Tobias Happ ================================================ FILE: modules/couchbase/build.gradle ================================================ description = "Testcontainers :: Couchbase" dependencies { api project(':testcontainers') // TODO use JDK's HTTP client and/or Apache HttpClient5 shaded 'com.squareup.okhttp3:okhttp:5.3.2' testImplementation 'com.couchbase.client:java-client:3.10.0' testImplementation 'org.awaitility:awaitility:4.3.0' } ================================================ FILE: modules/couchbase/src/main/java/org/testcontainers/couchbase/BucketDefinition.java ================================================ /* * Copyright (c) 2020 Couchbase, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.testcontainers.couchbase; /** * Allows to configure the properties of a bucket that should be created. */ public class BucketDefinition { private final String name; private boolean flushEnabled = false; private boolean queryPrimaryIndex = true; private int quota = 100; private int numReplicas = 0; public BucketDefinition(final String name) { this.name = name; } /** * Allows to configure the number of replicas on a bucket (defaults to 0). *

* By default the bucket is initialized with 0 replicas since only a single container is launched. Modifying * this value can still be useful in some test scenarios (i.e. to test failures with the wrong number of replicas * and durability requirements on operations). *

* Couchbase buckets can have a maximum of three replicas configured. * * @param numReplicas the number of replicas to configure. * @return this {@link BucketDefinition} for chaining purposes. */ public BucketDefinition withReplicas(final int numReplicas) { if (numReplicas < 0 || numReplicas > 3) { throw new IllegalArgumentException("The number of replicas must be between 0 and 3 (inclusive)"); } this.numReplicas = numReplicas; return this; } /** * Enables flush for this bucket (disabled by default). * * @param flushEnabled if true, the bucket can be flushed. * @return this {@link BucketDefinition} for chaining purposes. */ public BucketDefinition withFlushEnabled(final boolean flushEnabled) { this.flushEnabled = flushEnabled; return this; } /** * Sets a custom bucket quota (100MiB by default). * * @param quota the quota to set for the bucket in mebibytes. * @return this {@link BucketDefinition} for chaining purposes. */ public BucketDefinition withQuota(final int quota) { if (quota < 100) { throw new IllegalArgumentException("Bucket quota cannot be less than 100MB!"); } this.quota = quota; return this; } /** * Allows to disable creating a primary index for this bucket (enabled by default). * * @param create if false, a primary index will not be created. * @return this {@link BucketDefinition} for chaining purposes. */ public BucketDefinition withPrimaryIndex(final boolean create) { this.queryPrimaryIndex = create; return this; } public String getName() { return name; } public boolean hasFlushEnabled() { return flushEnabled; } public boolean hasPrimaryIndex() { return queryPrimaryIndex; } public int getQuota() { return quota; } public int getNumReplicas() { return numReplicas; } } ================================================ FILE: modules/couchbase/src/main/java/org/testcontainers/couchbase/CouchbaseContainer.java ================================================ /* * Copyright (c) 2020 Couchbase, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.testcontainers.couchbase; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.api.model.ContainerNetwork; import lombok.Cleanup; import okhttp3.Credentials; import okhttp3.FormBody; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import org.rnorth.ducttape.unreliables.Unreliables; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; import org.testcontainers.containers.wait.strategy.WaitAllStrategy; import org.testcontainers.utility.DockerImageName; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import java.util.stream.Collectors; /** * Testcontainers implementation for Couchbase. *

* Supported image: {@code couchbase/server} *

* Exposed ports: *

    *
  • Console: 8091
  • *
*

* Note that it does not depend on a specific couchbase SDK, so it can be used with both the Java SDK 2 and 3 as well * as the Scala SDK 1 or newer. We recommend using the latest and greatest SDKs for the best experience. */ public class CouchbaseContainer extends GenericContainer { private static final int MGMT_PORT = 8091; private static final int MGMT_SSL_PORT = 18091; private static final int VIEW_PORT = 8092; private static final int VIEW_SSL_PORT = 18092; private static final int QUERY_PORT = 8093; private static final int QUERY_SSL_PORT = 18093; private static final int SEARCH_PORT = 8094; private static final int SEARCH_SSL_PORT = 18094; private static final int ANALYTICS_PORT = 8095; private static final int ANALYTICS_SSL_PORT = 18095; private static final int EVENTING_PORT = 8096; private static final int EVENTING_SSL_PORT = 18096; private static final int KV_PORT = 11210; private static final int KV_SSL_PORT = 11207; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("couchbase/server"); private static final ObjectMapper MAPPER = new ObjectMapper(); private static final OkHttpClient HTTP_CLIENT = new OkHttpClient(); private String username = "Administrator"; private String password = "password"; /** * Enabled services does not include Analytics since most users likely do not need to test * with it and is also a little heavy on memory and runtime requirements. Also, it is only * available with the enterprise edition (EE). */ private Set enabledServices = EnumSet.of( CouchbaseService.KV, CouchbaseService.QUERY, CouchbaseService.SEARCH, CouchbaseService.INDEX ); /** * Holds the custom service quotas if configured by the user. */ private final Map customServiceQuotas = new HashMap<>(); private final List buckets = new ArrayList<>(); private boolean isEnterprise = false; private boolean hasTlsPorts = false; /** * Creates a new couchbase container with the specified image name. * * @param dockerImageName the image name that should be used. */ public CouchbaseContainer(final String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } /** * Create a new couchbase container with the specified image name. * @param dockerImageName the image name that should be used. */ public CouchbaseContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); } /** * Set custom username and password for the admin user. * * @param username the admin username to use. * @param password the password for the admin user. * @return this {@link CouchbaseContainer} for chaining purposes. */ public CouchbaseContainer withCredentials(final String username, final String password) { checkNotRunning(); this.username = username; this.password = password; return this; } public CouchbaseContainer withBucket(final BucketDefinition bucketDefinition) { checkNotRunning(); this.buckets.add(bucketDefinition); return this; } public CouchbaseContainer withEnabledServices(final CouchbaseService... enabled) { checkNotRunning(); this.enabledServices = EnumSet.copyOf(Arrays.asList(enabled)); return this; } /** * Configures a custom memory quota for a given service. * * @param service the service to configure the quota for. * @param quotaMb the memory quota in MB. * @return this {@link CouchbaseContainer} for chaining purposes. */ public CouchbaseContainer withServiceQuota(final CouchbaseService service, final int quotaMb) { checkNotRunning(); if (!service.hasQuota()) { throw new IllegalArgumentException("The provided service (" + service + ") has no quota to configure"); } if (quotaMb < service.getMinimumQuotaMb()) { throw new IllegalArgumentException( "The custom quota (" + quotaMb + ") must not be smaller than the " + "minimum quota for the service (" + service.getMinimumQuotaMb() + ")" ); } this.customServiceQuotas.put(service, quotaMb); return this; } /** * Enables the analytics service which is not enabled by default. * * @return this {@link CouchbaseContainer} for chaining purposes. */ public CouchbaseContainer withAnalyticsService() { checkNotRunning(); this.enabledServices.add(CouchbaseService.ANALYTICS); return this; } /** * Enables the eventing service which is not enabled by default. * * @return this {@link CouchbaseContainer} for chaining purposes. */ public CouchbaseContainer withEventingService() { checkNotRunning(); this.enabledServices.add(CouchbaseService.EVENTING); return this; } public final String getUsername() { return username; } public final String getPassword() { return password; } public int getBootstrapCarrierDirectPort() { return getMappedPort(KV_PORT); } public int getBootstrapHttpDirectPort() { return getMappedPort(MGMT_PORT); } public String getConnectionString() { return String.format("couchbase://%s:%d", getHost(), getBootstrapCarrierDirectPort()); } @Override protected void configure() { super.configure(); exposePorts(); WaitAllStrategy waitStrategy = new WaitAllStrategy(); // Makes sure that all nodes in the cluster are healthy. waitStrategy = waitStrategy.withStrategy( new HttpWaitStrategy() .forPath("/pools/default") .forPort(MGMT_PORT) .withBasicCredentials(username, password) .forStatusCode(200) .forResponsePredicate(response -> { try { return Optional .of(MAPPER.readTree(response)) .map(n -> n.at("/nodes/0/status")) .map(JsonNode::asText) .map("healthy"::equals) .orElse(false); } catch (IOException e) { logger().error("Unable to parse response: {}", response, e); return false; } }) ); if (enabledServices.contains(CouchbaseService.QUERY)) { waitStrategy = waitStrategy.withStrategy( new HttpWaitStrategy() .forPath("/admin/ping") .forPort(QUERY_PORT) .withBasicCredentials(username, password) .forStatusCode(200) ); } if (enabledServices.contains(CouchbaseService.ANALYTICS)) { waitStrategy = waitStrategy.withStrategy( new HttpWaitStrategy() .forPath("/admin/ping") .forPort(ANALYTICS_PORT) .withBasicCredentials(username, password) .forStatusCode(200) ); } if (enabledServices.contains(CouchbaseService.EVENTING)) { waitStrategy = waitStrategy.withStrategy( new HttpWaitStrategy() .forPath("/api/v1/config") .forPort(EVENTING_PORT) .withBasicCredentials(username, password) .forStatusCode(200) ); } waitingFor(waitStrategy); } /** * Configures the exposed ports based on the enabled services. *

* Note that the MGMT_PORTs are always enabled since there must always be a cluster * manager. Also, the View engine ports are implicitly available on the same nodes * where the KV service is enabled - it is not possible to configure them individually. */ private void exposePorts() { addExposedPorts(MGMT_PORT, MGMT_SSL_PORT); if (enabledServices.contains(CouchbaseService.KV)) { addExposedPorts(KV_PORT, KV_SSL_PORT); addExposedPorts(VIEW_PORT, VIEW_SSL_PORT); } if (enabledServices.contains(CouchbaseService.ANALYTICS)) { addExposedPorts(ANALYTICS_PORT, ANALYTICS_SSL_PORT); } if (enabledServices.contains(CouchbaseService.QUERY)) { addExposedPorts(QUERY_PORT, QUERY_SSL_PORT); } if (enabledServices.contains(CouchbaseService.SEARCH)) { addExposedPorts(SEARCH_PORT, SEARCH_SSL_PORT); } if (enabledServices.contains(CouchbaseService.EVENTING)) { addExposedPorts(EVENTING_PORT, EVENTING_SSL_PORT); } } @Override protected void containerIsStarting(InspectContainerResponse containerInfo, boolean reused) { if (!reused) { containerIsStarting(containerInfo); } } @Override protected void containerIsStarting(final InspectContainerResponse containerInfo) { logger().debug("Couchbase container is starting, performing configuration."); timePhase("waitUntilNodeIsOnline", this::waitUntilNodeIsOnline); timePhase("initializeIsEnterprise", this::initializeIsEnterprise); timePhase("initializeHasTlsPorts", this::initializeHasTlsPorts); timePhase("renameNode", this::renameNode); timePhase("initializeServices", this::initializeServices); timePhase("setMemoryQuotas", this::setMemoryQuotas); timePhase("configureAdminUser", this::configureAdminUser); timePhase("configureExternalPorts", this::configureExternalPorts); if (enabledServices.contains(CouchbaseService.INDEX)) { timePhase("configureIndexer", this::configureIndexer); } } @Override protected void containerIsStarted(InspectContainerResponse containerInfo, boolean reused) { if (!reused) { this.containerIsStarted(containerInfo); } } @Override protected void containerIsStarted(InspectContainerResponse containerInfo) { timePhase("createBuckets", this::createBuckets); logger() .info("Couchbase container is ready! UI available at http://{}:{}", getHost(), getMappedPort(MGMT_PORT)); } /** * Before we can start configuring the host, we need to wait until the cluster manager is listening. */ private void waitUntilNodeIsOnline() { new HttpWaitStrategy().forPort(MGMT_PORT).forPath("/pools").forStatusCode(200).waitUntilReady(this); } /** * Fetches edition (enterprise or community) of started container. */ private void initializeIsEnterprise() { @Cleanup Response response = doHttpRequest(MGMT_PORT, "/pools", "GET", null, true); try { isEnterprise = MAPPER.readTree(response.body().string()).get("isEnterprise").asBoolean(); } catch (IOException e) { throw new IllegalStateException("Couchbase /pools did not return valid JSON"); } if (!isEnterprise) { if (enabledServices.contains(CouchbaseService.ANALYTICS)) { throw new IllegalStateException("The Analytics Service is only supported with the Enterprise version"); } if (enabledServices.contains(CouchbaseService.EVENTING)) { throw new IllegalStateException("The Eventing Service is only supported with the Enterprise version"); } } } /** * Initializes the {@link #hasTlsPorts} flag. *

* Community Edition might support TLS one happy day, so use a "supports TLS" flag separate from * the "enterprise edition" flag. */ private void initializeHasTlsPorts() { @Cleanup Response response = doHttpRequest(MGMT_PORT, "/pools/default/nodeServices", "GET", null, true); try { String clusterTopology = response.body().string(); hasTlsPorts = !MAPPER .readTree(clusterTopology) .path("nodesExt") .path(0) .path("services") .path("mgmtSSL") .isMissingNode(); } catch (IOException e) { throw new IllegalStateException("Couchbase /pools/default/nodeServices did not return valid JSON"); } } /** * Rebinds/renames the internal hostname. *

* To make sure the internal hostname is different from the external (alternate) address and the SDK can pick it * up automatically, we bind the internal hostname to the internal IP address. */ private void renameNode() { logger().debug("Renaming Couchbase Node from localhost to {}", getHost()); @Cleanup Response response = doHttpRequest( MGMT_PORT, "/node/controller/rename", "POST", new FormBody.Builder().add("hostname", getInternalIpAddress()).build(), false ); checkSuccessfulResponse(response, "Could not rename couchbase node"); } /** * Initializes services based on the configured enabled services. */ private void initializeServices() { logger().debug("Initializing couchbase services on host: {}", enabledServices); final String services = enabledServices .stream() .map(CouchbaseService::getIdentifier) .collect(Collectors.joining(",")); @Cleanup Response response = doHttpRequest( MGMT_PORT, "/node/controller/setupServices", "POST", new FormBody.Builder().add("services", services).build(), false ); checkSuccessfulResponse(response, "Could not enable couchbase services"); } /** * Sets the memory quotas for each enabled service. *

* If there is no explicit custom quota defined, the default minimum quota will be used. */ private void setMemoryQuotas() { logger().debug("Custom service memory quotas: {}", customServiceQuotas); final FormBody.Builder quotaBuilder = new FormBody.Builder(); for (CouchbaseService service : enabledServices) { if (!service.hasQuota()) { continue; } int quota = customServiceQuotas.getOrDefault(service, service.getMinimumQuotaMb()); if (CouchbaseService.KV.equals(service)) { quotaBuilder.add("memoryQuota", Integer.toString(quota)); } else { quotaBuilder.add(service.getIdentifier() + "MemoryQuota", Integer.toString(quota)); } } @Cleanup Response response = doHttpRequest(MGMT_PORT, "/pools/default", "POST", quotaBuilder.build(), false); checkSuccessfulResponse(response, "Could not configure service memory quotas"); } /** * Configures the admin user on the couchbase node. *

* After this stage, all subsequent API calls need to have the basic auth header set. */ private void configureAdminUser() { logger().debug("Configuring couchbase admin user with username: \"{}\"", username); @Cleanup Response response = doHttpRequest( MGMT_PORT, "/settings/web", "POST", new FormBody.Builder() .add("username", username) .add("password", password) .add("port", Integer.toString(MGMT_PORT)) .build(), false ); checkSuccessfulResponse(response, "Could not configure couchbase admin user"); } /** * Configures the external ports for SDK access. *

* Since the internal ports are not accessible from outside the container, this code configures the "external" * hostname and services to align with the mapped ports. The SDK will pick it up and then automatically connect * to those ports. Note that for all services non-ssl and ssl ports are configured. */ private void configureExternalPorts() { logger().debug("Mapping external ports to the alternate address configuration"); final FormBody.Builder builder = new FormBody.Builder(); builder.add("hostname", getHost()); builder.add("mgmt", Integer.toString(getMappedPort(MGMT_PORT))); if (hasTlsPorts) { builder.add("mgmtSSL", Integer.toString(getMappedPort(MGMT_SSL_PORT))); } if (enabledServices.contains(CouchbaseService.KV)) { builder.add("kv", Integer.toString(getMappedPort(KV_PORT))); builder.add("capi", Integer.toString(getMappedPort(VIEW_PORT))); if (hasTlsPorts) { builder.add("kvSSL", Integer.toString(getMappedPort(KV_SSL_PORT))); builder.add("capiSSL", Integer.toString(getMappedPort(VIEW_SSL_PORT))); } } if (enabledServices.contains(CouchbaseService.QUERY)) { builder.add("n1ql", Integer.toString(getMappedPort(QUERY_PORT))); if (hasTlsPorts) { builder.add("n1qlSSL", Integer.toString(getMappedPort(QUERY_SSL_PORT))); } } if (enabledServices.contains(CouchbaseService.SEARCH)) { builder.add("fts", Integer.toString(getMappedPort(SEARCH_PORT))); if (hasTlsPorts) { builder.add("ftsSSL", Integer.toString(getMappedPort(SEARCH_SSL_PORT))); } } if (enabledServices.contains(CouchbaseService.ANALYTICS)) { builder.add("cbas", Integer.toString(getMappedPort(ANALYTICS_PORT))); if (hasTlsPorts) { builder.add("cbasSSL", Integer.toString(getMappedPort(ANALYTICS_SSL_PORT))); } } if (enabledServices.contains(CouchbaseService.EVENTING)) { builder.add("eventingAdminPort", Integer.toString(getMappedPort(EVENTING_PORT))); if (hasTlsPorts) { builder.add("eventingSSL", Integer.toString(getMappedPort(EVENTING_SSL_PORT))); } } @Cleanup Response response = doHttpRequest( MGMT_PORT, "/node/controller/setupAlternateAddresses/external", "PUT", builder.build(), true ); checkSuccessfulResponse(response, "Could not configure external ports"); } /** * Configures the indexer service so that indexes can be created later on the bucket. */ private void configureIndexer() { logger().debug("Configuring the indexer service"); @Cleanup Response response = doHttpRequest( MGMT_PORT, "/settings/indexes", "POST", new FormBody.Builder().add("storageMode", isEnterprise ? "memory_optimized" : "forestdb").build(), true ); checkSuccessfulResponse(response, "Could not configure the indexing service"); } /** * Based on the user-configured bucket definitions, creating buckets and corresponding indexes if needed. */ private void createBuckets() { logger().debug("Creating {} buckets (and corresponding indexes).", buckets.size()); for (BucketDefinition bucket : buckets) { logger().debug("Creating bucket \"{}\"", bucket.getName()); @Cleanup Response response = doHttpRequest( MGMT_PORT, "/pools/default/buckets", "POST", new FormBody.Builder() .add("name", bucket.getName()) .add("ramQuotaMB", Integer.toString(bucket.getQuota())) .add("flushEnabled", bucket.hasFlushEnabled() ? "1" : "0") .add("replicaNumber", Integer.toString(bucket.getNumReplicas())) .build(), true ); checkSuccessfulResponse(response, "Could not create bucket " + bucket.getName()); timePhase( "createBucket:" + bucket.getName() + ":waitForAllServicesEnabled", () -> { new HttpWaitStrategy() .forPath("/pools/default/b/" + bucket.getName()) .forPort(MGMT_PORT) .withBasicCredentials(username, password) .forStatusCode(200) .forResponsePredicate(new AllServicesEnabledPredicate()) .waitUntilReady(this); } ); if (enabledServices.contains(CouchbaseService.QUERY)) { // If the query service is enabled, make sure that we only proceed if the query engine also // knows about the bucket in its metadata configuration. timePhase( "createBucket:" + bucket.getName() + ":queryKeyspacePresent", () -> { Unreliables.retryUntilTrue( 1, TimeUnit.MINUTES, () -> { @Cleanup Response queryResponse = doHttpRequest( QUERY_PORT, "/query/service", "POST", new FormBody.Builder() .add( "statement", "SELECT COUNT(*) > 0 as present FROM system:keyspaces WHERE name = \"" + bucket.getName() + "\"" ) .build(), true ); String body = queryResponse.body() != null ? queryResponse.body().string() : null; checkSuccessfulResponse( queryResponse, "Could not poll query service state for bucket: " + bucket.getName() ); return Optional .of(MAPPER.readTree(body)) .map(n -> n.at("/results/0/present")) .map(JsonNode::asBoolean) .orElse(false); } ); } ); } if (bucket.hasPrimaryIndex()) { if (enabledServices.contains(CouchbaseService.QUERY)) { @Cleanup Response queryResponse = doHttpRequest( QUERY_PORT, "/query/service", "POST", new FormBody.Builder() .add("statement", "CREATE PRIMARY INDEX on `" + bucket.getName() + "`") .build(), true ); try { checkSuccessfulResponse( queryResponse, "Could not create primary index for bucket " + bucket.getName() ); } catch (IllegalStateException ex) { // potentially ignore the error, the index will be eventually built. if (!ex.getMessage().contains("Index creation will be retried in background")) { throw ex; } } timePhase( "createBucket:" + bucket.getName() + ":primaryIndexOnline", () -> { Unreliables.retryUntilTrue( 1, TimeUnit.MINUTES, () -> { @Cleanup Response stateResponse = doHttpRequest( QUERY_PORT, "/query/service", "POST", new FormBody.Builder() .add( "statement", "SELECT count(*) > 0 AS online FROM system:indexes where keyspace_id = \"" + bucket.getName() + "\" and is_primary = true and state = \"online\"" ) .build(), true ); String body = stateResponse.body() != null ? stateResponse.body().string() : null; checkSuccessfulResponse( stateResponse, "Could not poll primary index state for bucket: " + bucket.getName() ); return Optional .of(MAPPER.readTree(body)) .map(n -> n.at("/results/0/online")) .map(JsonNode::asBoolean) .orElse(false); } ); } ); } else { logger() .info( "Primary index creation for bucket {} ignored, since QUERY service is not present.", bucket.getName() ); } } } } /** * Helper method to extract the internal IP address based on the network configuration. */ private String getInternalIpAddress() { return getContainerInfo() .getNetworkSettings() .getNetworks() .values() .stream() .findFirst() .map(ContainerNetwork::getIpAddress) .orElseThrow(() -> new IllegalStateException("No network available to extract the internal IP from!")); } /** * Helper method to check if the response is successful and release the body if needed. * * @param response the response to check. * @param message the message that should be part of the exception of not successful. */ private void checkSuccessfulResponse(final Response response, final String message) { if (!response.isSuccessful()) { String body = null; if (response.body() != null) { try { body = response.body().string(); } catch (IOException e) { logger().debug("Unable to read body of response: {}", response, e); } } throw new IllegalStateException(message + ": " + response + ", body=" + (body == null ? "" : body)); } } /** * Checks if already running and if so raises an exception to prevent too-late setters. */ private void checkNotRunning() { if (isRunning()) { throw new IllegalStateException("Setter can only be called before the container is running"); } } /** * Helper method to perform a request against a couchbase server HTTP endpoint. * * @param port the (unmapped) original port that should be used. * @param path the relative http path. * @param method the http method to use. * @param body if present, will be part of the payload. * @param auth if authentication with the admin user and password should be used. * @return the response of the request. */ private Response doHttpRequest( final int port, final String path, final String method, final RequestBody body, final boolean auth ) { try { Request.Builder requestBuilder = new Request.Builder() .url("http://" + getHost() + ":" + getMappedPort(port) + path); if (auth) { requestBuilder = requestBuilder.header("Authorization", Credentials.basic(username, password)); } if (body == null) { requestBuilder = requestBuilder.get(); } else { requestBuilder = requestBuilder.method(method, body); } return HTTP_CLIENT.newCall(requestBuilder.build()).execute(); } catch (Exception ex) { throw new RuntimeException("Could not perform request against couchbase HTTP endpoint ", ex); } } /** * Helper method which times an individual phase and logs it for debugging and optimization purposes. * * @param name the name of the phase. * @param toTime the runnable that should be timed. */ private void timePhase(final String name, final Runnable toTime) { long start = System.nanoTime(); toTime.run(); long end = System.nanoTime(); logger().debug("Phase {} took {}ms", name, TimeUnit.NANOSECONDS.toMillis(end - start)); } /** * In addition to getting a 200, we need to make sure that all services we need are enabled and available on * the bucket. *

* Fixes the issue observed in https://github.com/testcontainers/testcontainers-java/issues/2993 */ private class AllServicesEnabledPredicate implements Predicate { @Override public boolean test(final String rawConfig) { try { for (JsonNode node : MAPPER.readTree(rawConfig).at("/nodesExt")) { for (CouchbaseService enabledService : enabledServices) { boolean found = false; Iterator fieldNames = node.get("services").fieldNames(); while (fieldNames.hasNext()) { if (fieldNames.next().startsWith(enabledService.getIdentifier())) { found = true; } } if (!found) { logger().trace("Service {} not yet part of config, retrying.", enabledService); return false; } } } return true; } catch (IOException ex) { logger().error("Unable to parse response: {}", rawConfig, ex); return false; } } } } ================================================ FILE: modules/couchbase/src/main/java/org/testcontainers/couchbase/CouchbaseService.java ================================================ /* * Copyright (c) 2020 Couchbase, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.testcontainers.couchbase; /** * Describes the different services that should be exposed on the container. */ public enum CouchbaseService { /** * Key-Value service. */ KV("kv", 256), /** * Query (N1QL) service. *

* Note that the query service has no memory quota, so it is set to 0. */ QUERY("n1ql", 0), /** * Search (FTS) service. */ SEARCH("fts", 256), /** * Indexing service (needed if QUERY is also used!). */ INDEX("index", 256), /** * Analytics service. */ ANALYTICS("cbas", 1024), /** * Eventing service. */ EVENTING("eventing", 256); private final String identifier; private final int minimumQuotaMb; CouchbaseService(final String identifier, final int minimumQuotaMb) { this.identifier = identifier; this.minimumQuotaMb = minimumQuotaMb; } /** * Returns the internal service identifier. * * @return the internal service identifier. */ String getIdentifier() { return identifier; } /** * Returns the minimum quota for the service in MB. * * @return the minimum quota in MB. */ int getMinimumQuotaMb() { return minimumQuotaMb; } /** * Returns true if the service has a quota that needs to be applied. * * @return true if its quota needs to be applied. */ boolean hasQuota() { return minimumQuotaMb > 0; } } ================================================ FILE: modules/couchbase/src/test/java/org/testcontainers/couchbase/CouchbaseContainerTest.java ================================================ /* * Copyright (c) 2020 Couchbase, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.testcontainers.couchbase; import com.couchbase.client.java.Bucket; import com.couchbase.client.java.Cluster; import com.couchbase.client.java.Collection; import com.couchbase.client.java.json.JsonObject; import org.junit.jupiter.api.Test; import org.testcontainers.containers.ContainerLaunchException; import java.time.Duration; import java.util.function.Consumer; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; class CouchbaseContainerTest { private static final String COUCHBASE_IMAGE_ENTERPRISE = "couchbase/server:enterprise-7.0.3"; private static final String COUCHBASE_IMAGE_ENTERPRISE_RECENT = "couchbase/server:enterprise-7.6.2"; private static final String COUCHBASE_IMAGE_COMMUNITY = "couchbase/server:community-7.0.2"; private static final String COUCHBASE_IMAGE_COMMUNITY_RECENT = "couchbase/server:community-7.6.2"; @Test void testBasicContainerUsageForEnterpriseContainer() { testBasicContainerUsage(COUCHBASE_IMAGE_ENTERPRISE); } @Test void testBasicContainerUsageForEnterpriseContainerRecent() { testBasicContainerUsage(COUCHBASE_IMAGE_ENTERPRISE_RECENT); } @Test void testBasicContainerUsageForCommunityContainer() { testBasicContainerUsage(COUCHBASE_IMAGE_COMMUNITY); } @Test void testBasicContainerUsageForCommunityContainerRecent() { testBasicContainerUsage(COUCHBASE_IMAGE_COMMUNITY_RECENT); } private void testBasicContainerUsage(String couchbaseImage) { // bucket_definition { BucketDefinition bucketDefinition = new BucketDefinition("mybucket"); // } try ( // container_definition { CouchbaseContainer container = new CouchbaseContainer(couchbaseImage).withBucket(bucketDefinition) // } ) { setUpClient( container, cluster -> { Bucket bucket = cluster.bucket(bucketDefinition.getName()); bucket.waitUntilReady(Duration.ofSeconds(10L)); Collection collection = bucket.defaultCollection(); collection.upsert("foo", JsonObject.create().put("key", "value")); JsonObject fooObject = collection.get("foo").contentAsObject(); assertThat(fooObject.getString("key")).isEqualTo("value"); } ); } } @Test void testBucketIsFlushableIfEnabled() { BucketDefinition bucketDefinition = new BucketDefinition("mybucket").withFlushEnabled(true); try ( CouchbaseContainer container = new CouchbaseContainer(COUCHBASE_IMAGE_ENTERPRISE) .withBucket(bucketDefinition) ) { setUpClient( container, cluster -> { Bucket bucket = cluster.bucket(bucketDefinition.getName()); bucket.waitUntilReady(Duration.ofSeconds(10L)); Collection collection = bucket.defaultCollection(); collection.upsert("foo", JsonObject.create().put("key", "value")); cluster.buckets().flushBucket(bucketDefinition.getName()); await().untilAsserted(() -> assertThat(collection.exists("foo").exists()).isFalse()); } ); } } /** * Make sure that the code fails fast if the Analytics service is enabled on the community * edition which is not supported. */ @Test void testFailureIfCommunityUsedWithAnalytics() { try ( CouchbaseContainer container = new CouchbaseContainer(COUCHBASE_IMAGE_COMMUNITY) .withEnabledServices(CouchbaseService.KV, CouchbaseService.ANALYTICS) ) { assertThatThrownBy(() -> { setUpClient(container, cluster -> {}); }) .isInstanceOf(ContainerLaunchException.class); } } /** * Make sure that the code fails fast if the Eventing service is enabled on the community * edition which is not supported. */ @Test void testFailureIfCommunityUsedWithEventing() { try ( CouchbaseContainer container = new CouchbaseContainer(COUCHBASE_IMAGE_COMMUNITY) .withEnabledServices(CouchbaseService.KV, CouchbaseService.EVENTING) ) { assertThatThrownBy(() -> { setUpClient(container, cluster -> {}); }) .isInstanceOf(ContainerLaunchException.class); } } private void setUpClient(CouchbaseContainer container, Consumer consumer) { container.start(); // cluster_creation { Cluster cluster = Cluster.connect( container.getConnectionString(), container.getUsername(), container.getPassword() ); // } try { consumer.accept(cluster); } finally { cluster.disconnect(); } } } ================================================ FILE: modules/couchbase/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/cratedb/build.gradle ================================================ description = "Testcontainers :: JDBC :: CrateDB" dependencies { api project(':testcontainers-jdbc') testRuntimeOnly 'org.postgresql:postgresql:42.7.8' testImplementation project(':testcontainers-jdbc-test') compileOnly 'org.jetbrains:annotations:26.0.2-1' } ================================================ FILE: modules/cratedb/src/main/java/org/testcontainers/cratedb/CrateDBContainer.java ================================================ package org.testcontainers.cratedb; import org.jetbrains.annotations.NotNull; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import java.util.Set; /** * Testcontainers implementation for CrateDB. *

* Supported image: {@code crate} *

* Exposed ports: *

    *
  • Database: 5432
  • *
  • Console: 4200
  • *
*/ public class CrateDBContainer extends JdbcDatabaseContainer { static final String NAME = "cratedb"; static final String IMAGE = "crate"; static final String DEFAULT_TAG = "5.3.1"; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("crate"); static final Integer CRATEDB_PG_PORT = 5432; static final Integer CRATEDB_HTTP_PORT = 4200; private String databaseName = "crate"; private String username = "crate"; private String password = "crate"; public CrateDBContainer(final String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public CrateDBContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); withCommand("crate -C discovery.type=single-node"); waitingFor(Wait.forHttp("/").forPort(CRATEDB_HTTP_PORT).forStatusCode(200)); addExposedPort(CRATEDB_PG_PORT); addExposedPort(CRATEDB_HTTP_PORT); } /** * @return the ports on which to check if the container is ready * @deprecated use {@link #getLivenessCheckPortNumbers()} instead */ @NotNull @Override @Deprecated protected Set getLivenessCheckPorts() { return super.getLivenessCheckPorts(); } @Override public String getDriverClassName() { return "org.postgresql.Driver"; } @Override public String getJdbcUrl() { String additionalUrlParams = constructUrlParameters("?", "&"); return ( "jdbc:postgresql://" + getHost() + ":" + getMappedPort(CRATEDB_PG_PORT) + "/" + databaseName + additionalUrlParams ); } @Override public String getDatabaseName() { return databaseName; } @Override public String getUsername() { return username; } @Override public String getPassword() { return password; } @Override public String getTestQueryString() { return "SELECT 1"; } @Override public CrateDBContainer withDatabaseName(final String databaseName) { this.databaseName = databaseName; return self(); } @Override public CrateDBContainer withUsername(final String username) { this.username = username; return self(); } @Override public CrateDBContainer withPassword(final String password) { this.password = password; return self(); } @Override protected void waitUntilContainerStarted() { getWaitStrategy().waitUntilReady(this); } } ================================================ FILE: modules/cratedb/src/main/java/org/testcontainers/cratedb/CrateDBContainerProvider.java ================================================ package org.testcontainers.cratedb; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.containers.JdbcDatabaseContainerProvider; import org.testcontainers.jdbc.ConnectionUrl; import org.testcontainers.utility.DockerImageName; /** * Factory for CrateDB containers using PostgreSQL JDBC driver. */ public class CrateDBContainerProvider extends JdbcDatabaseContainerProvider { public static final String USER_PARAM = "user"; public static final String PASSWORD_PARAM = "password"; @Override public boolean supports(String databaseType) { return databaseType.equals(CrateDBContainer.NAME); } @Override public JdbcDatabaseContainer newInstance() { return newInstance(CrateDBContainer.DEFAULT_TAG); } @Override public JdbcDatabaseContainer newInstance(String tag) { return new CrateDBContainer(DockerImageName.parse(CrateDBContainer.IMAGE).withTag(tag)); } @Override public JdbcDatabaseContainer newInstance(ConnectionUrl connectionUrl) { return newInstanceFromConnectionUrl(connectionUrl, USER_PARAM, PASSWORD_PARAM); } } ================================================ FILE: modules/cratedb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider ================================================ org.testcontainers.cratedb.CrateDBContainerProvider ================================================ FILE: modules/cratedb/src/test/java/org/testcontainers/CrateDBTestImages.java ================================================ package org.testcontainers; import org.testcontainers.utility.DockerImageName; public interface CrateDBTestImages { DockerImageName CRATEDB_TEST_IMAGE = DockerImageName.parse("crate:5.2.5"); } ================================================ FILE: modules/cratedb/src/test/java/org/testcontainers/jdbc/cratedb/CrateDBJDBCDriverTest.java ================================================ package org.testcontainers.jdbc.cratedb; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; class CrateDBJDBCDriverTest extends AbstractJDBCDriverTest { public static Iterable data() { return Arrays.asList( new Object[][] { { "jdbc:tc:cratedb:5.2.3://hostname/crate?user=crate&password=somepwd", EnumSet.noneOf(Options.class) }, } ); } } ================================================ FILE: modules/cratedb/src/test/java/org/testcontainers/junit/cratedb/SimpleCrateDBTest.java ================================================ package org.testcontainers.junit.cratedb; import org.junit.jupiter.api.Test; import org.testcontainers.CrateDBTestImages; import org.testcontainers.cratedb.CrateDBContainer; import org.testcontainers.db.AbstractContainerDatabaseTest; import java.sql.ResultSet; import java.sql.SQLException; import java.util.logging.Level; import java.util.logging.LogManager; import static org.assertj.core.api.Assertions.assertThat; class SimpleCrateDBTest extends AbstractContainerDatabaseTest { static { // Postgres JDBC driver uses JUL; disable it to avoid annoying, irrelevant, stderr logs during connection testing LogManager.getLogManager().getLogger("").setLevel(Level.OFF); } @Test void testSimple() throws SQLException { try ( // container { CrateDBContainer cratedb = new CrateDBContainer("crate:5.2.5") // } ) { cratedb.start(); ResultSet resultSet = performQuery(cratedb, "SELECT 1"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); assertHasCorrectExposedAndLivenessCheckPorts(cratedb); } } @Test void testCommandOverride() throws SQLException { try ( CrateDBContainer cratedb = new CrateDBContainer(CrateDBTestImages.CRATEDB_TEST_IMAGE) .withCommand("crate -C discovery.type=single-node -C cluster.name=testcontainers") ) { cratedb.start(); ResultSet resultSet = performQuery(cratedb, "select name from sys.cluster"); String result = resultSet.getString(1); assertThat(result).as("cluster name should be overridden").isEqualTo("testcontainers"); } } @Test void testExplicitInitScript() throws SQLException { try ( CrateDBContainer cratedb = new CrateDBContainer(CrateDBTestImages.CRATEDB_TEST_IMAGE) .withInitScript("somepath/init_cratedb.sql") ) { cratedb.start(); ResultSet resultSet = performQuery(cratedb, "SELECT foo FROM bar"); String firstColumnValue = resultSet.getString(1); assertThat(firstColumnValue).as("Value from init script should equal real value").isEqualTo("hello world"); } } private void assertHasCorrectExposedAndLivenessCheckPorts(CrateDBContainer cratedb) { assertThat(cratedb.getExposedPorts()).containsExactlyInAnyOrder(5432, 4200); assertThat(cratedb.getLivenessCheckPortNumbers()) .containsExactlyInAnyOrder(cratedb.getMappedPort(5432), cratedb.getMappedPort(4200)); } } ================================================ FILE: modules/cratedb/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/cratedb/src/test/resources/somepath/init_cratedb.sql ================================================ CREATE TABLE bar ( foo STRING ); INSERT INTO bar (foo) VALUES ('hello world'); REFRESH TABLE bar; ================================================ FILE: modules/database-commons/build.gradle ================================================ description = "Testcontainers :: Database-Commons" dependencies { api project(':testcontainers') } ================================================ FILE: modules/database-commons/src/main/java/org/testcontainers/delegate/AbstractDatabaseDelegate.java ================================================ package org.testcontainers.delegate; import java.util.Collection; /** * @param connection to the database */ public abstract class AbstractDatabaseDelegate implements DatabaseDelegate { /** * Database connection */ private CONNECTION connection; private boolean isConnectionStarted = false; /** * Get or create new connection to the database */ protected CONNECTION getConnection() { if (!isConnectionStarted) { connection = createNewConnection(); isConnectionStarted = true; } return connection; } @Override public void execute( Collection statements, String scriptPath, boolean continueOnError, boolean ignoreFailedDrops ) { int lineNumber = 0; for (String statement : statements) { lineNumber++; execute(statement, scriptPath, lineNumber, continueOnError, ignoreFailedDrops); } } @Override public void close() { if (isConnectionStarted) { closeConnectionQuietly(connection); isConnectionStarted = false; } } /** * Quietly close the connection */ protected abstract void closeConnectionQuietly(CONNECTION connection); /** * Template method for creating new connections to the database */ protected abstract CONNECTION createNewConnection(); } ================================================ FILE: modules/database-commons/src/main/java/org/testcontainers/delegate/DatabaseDelegate.java ================================================ package org.testcontainers.delegate; import java.util.Collection; /** * Database delegate * * Gives an abstraction from concrete database */ public interface DatabaseDelegate extends AutoCloseable { /** * Execute statement by the implementation of the delegate */ void execute( String statement, String scriptPath, int lineNumber, boolean continueOnError, boolean ignoreFailedDrops ); /** * Execute collection of statements */ void execute(Collection statements, String scriptPath, boolean continueOnError, boolean ignoreFailedDrops); /** * Close connection to the database * * Overridden to suppress throwing Exception */ @Override void close(); } ================================================ FILE: modules/database-commons/src/main/java/org/testcontainers/exception/ConnectionCreationException.java ================================================ package org.testcontainers.exception; /** * Inability to create connection to the database */ public class ConnectionCreationException extends RuntimeException { public ConnectionCreationException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: modules/database-commons/src/main/java/org/testcontainers/ext/ScriptScanner.java ================================================ package org.testcontainers.ext; import lombok.Getter; import lombok.RequiredArgsConstructor; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Rough lexical parser for SQL scripts. */ @RequiredArgsConstructor class ScriptScanner { private final String resource; private final String script; private final String separator; private final String commentPrefix; private final String blockCommentStartDelimiter; private final String blockCommentEndDelimiter; private final Pattern eol = Pattern.compile("[\n\r]+"); private final Pattern whitespace = Pattern.compile("\\s+"); private final Pattern identifier = Pattern.compile("[a-z][a-z0-9_$]*", Pattern.CASE_INSENSITIVE); private final Pattern dollarQuotedStringDelimiter = Pattern.compile("\\$\\w*\\$"); private int offset; @Getter private String currentMatch; private boolean matches(String substring) { if (script.startsWith(substring, offset)) { currentMatch = substring; offset += currentMatch.length(); return true; } else { currentMatch = ""; return false; } } private boolean matches(Pattern regexp) { Matcher m = regexp.matcher(script); m.region(offset, script.length()); if (m.lookingAt()) { currentMatch = m.group(); offset = m.end(); return true; } else { currentMatch = ""; return false; } } private boolean matchesSingleLineComment() { /* Matches from commentPrefix to the EOL or end of script */ if (matches(commentPrefix)) { Matcher m = eol.matcher(script); if (m.find(offset)) { currentMatch = commentPrefix + script.substring(offset, m.end()); offset = m.end(); } else { currentMatch = commentPrefix + script.substring(offset); offset = script.length(); } return true; } return false; } private boolean matchesMultilineComment() { /* Matches from blockCommentStartDelimiter to the next blockCommentEndDelimiter. * Error, if blockCommentEndDelimiter is not found. */ if (matches(blockCommentStartDelimiter)) { int end = script.indexOf(blockCommentEndDelimiter, offset); if (end < 0) { throw new ScriptUtils.ScriptParseException( String.format("Missing block comment end delimiter [%s].", blockCommentEndDelimiter), resource ); } end += blockCommentEndDelimiter.length(); currentMatch = blockCommentStartDelimiter + script.substring(offset, end); offset = end; return true; } return false; } private boolean matchesQuotedString(final char quote) { if (script.charAt(offset) == quote) { boolean escaped = false; for (int i = offset + 1; i < script.length(); i++) { char c = script.charAt(i); if (escaped) { //just skip the escaped character and drop the flag escaped = false; } else if (c == '\\') { escaped = true; } else if (c == quote) { currentMatch = script.substring(offset, i + 1); offset = i + 1; return true; } } } return false; } private boolean matchesDollarQuotedString() { //Matches $$ .... $$ if (matches(dollarQuotedStringDelimiter)) { String delimiter = currentMatch; int end = script.indexOf(delimiter, offset); if (end < 0) { throw new ScriptUtils.ScriptParseException( String.format("Unclosed dollar quoted string [%s].", delimiter), resource ); } end += delimiter.length(); currentMatch = delimiter + script.substring(offset, end); offset = end; return true; } return false; } Lexem next() { if (offset < script.length()) { if (matches(separator)) { return Lexem.SEPARATOR; } else if (matchesSingleLineComment() || matchesMultilineComment()) { return Lexem.COMMENT; } else if ( matchesQuotedString('\'') || matchesQuotedString('"') || matchesQuotedString('`') || matchesDollarQuotedString() ) { return Lexem.QUOTED_STRING; } else if (matches(identifier)) { return Lexem.IDENTIFIER; } else if (matches(whitespace)) { return Lexem.WHITESPACE; } else { currentMatch = String.valueOf(script.charAt(offset++)); return Lexem.OTHER; } } else { return Lexem.EOF; } } enum Lexem { SEPARATOR, COMMENT, QUOTED_STRING, WHITESPACE, IDENTIFIER, OTHER, EOF, } } ================================================ FILE: modules/database-commons/src/main/java/org/testcontainers/ext/ScriptSplitter.java ================================================ package org.testcontainers.ext; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.testcontainers.ext.ScriptScanner.Lexem; import java.util.List; /** * Performs splitting of an SQL script into statements including * basic clean-up. */ @RequiredArgsConstructor class ScriptSplitter { private final ScriptScanner scanner; private final List statements; private final StringBuilder sb = new StringBuilder(); /** * Standard parsing: * 1. Remove comments * 2. Shrink whitespace and eols * 3. Split on separator */ void split() { Lexem l; while ((l = scanner.next()) != Lexem.EOF) { switch (l) { case SEPARATOR: flushStringBuilder(); break; case COMMENT: //skip break; case WHITESPACE: if (sb.length() == 0 || sb.charAt(sb.length() - 1) != ' ') { sb.append(' '); } break; case IDENTIFIER: appendMatch(); if ("begin".equalsIgnoreCase(scanner.getCurrentMatch())) { compoundStatement(false); flushStringBuilder(); } break; default: appendMatch(); } } flushStringBuilder(); } /** * Compound statement ('create procedure') mode: * 1. Do not remove comments * 2. Do not shrink whitespace * 3. Do not split on separators * 3. This mode can be recursive */ private void compoundStatement(boolean recursive) { Lexem l; while ((l = scanner.next()) != Lexem.EOF) { appendMatch(); if (Lexem.IDENTIFIER.equals(l)) { if ("begin".equalsIgnoreCase(scanner.getCurrentMatch())) { compoundStatement(true); } else if ("end".equalsIgnoreCase(scanner.getCurrentMatch())) { if (endOfBlock(recursive)) { return; } } } } flushStringBuilder(); } private boolean endOfBlock(boolean recursive) { Lexem l; StringBuilder temporary = new StringBuilder(); while ((l = scanner.next()) != Lexem.EOF) { switch (l) { case COMMENT: case WHITESPACE: temporary.append(scanner.getCurrentMatch()); break; case SEPARATOR: //Only whitespace and comments preceded the separator: true end of block //If it's an internal block, append everything if (recursive) { sb.append(temporary); appendMatch(); } return true; default: // Semicolon is not recognized as separator: this means that a custom // separator is used. Still, 'END;' should be a valid end of block if (";".equals(scanner.getCurrentMatch())) { if (recursive) { sb.append(temporary); } appendMatch(); return true; } sb.append(temporary); appendMatch(); return false; } } return true; } private void appendMatch() { sb.append(scanner.getCurrentMatch()); } private void flushStringBuilder() { final String s = sb.toString().trim(); if (StringUtils.isNotEmpty(s)) { statements.add(s); } sb.setLength(0); } } ================================================ FILE: modules/database-commons/src/main/java/org/testcontainers/ext/ScriptUtils.java ================================================ /* * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.testcontainers.ext; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.delegate.DatabaseDelegate; import java.io.IOException; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.LinkedList; import java.util.List; import java.util.concurrent.TimeUnit; import javax.script.ScriptException; /** * This is a modified version of the Spring-JDBC ScriptUtils class, adapted to reduce * dependencies and slightly alter the API. * * Generic utility methods for working with SQL scripts. Mainly for internal use * within the framework. */ public abstract class ScriptUtils { private static final Logger LOGGER = LoggerFactory.getLogger(ScriptUtils.class); /** * Default statement separator within SQL scripts. */ public static final String DEFAULT_STATEMENT_SEPARATOR = ";"; /** * Fallback statement separator within SQL scripts. *

Used if neither a custom defined separator nor the * {@link #DEFAULT_STATEMENT_SEPARATOR} is present in a given script. */ public static final String FALLBACK_STATEMENT_SEPARATOR = "\n"; /** * Default prefix for line comments within SQL scripts. */ public static final String DEFAULT_COMMENT_PREFIX = "--"; /** * Default start delimiter for block comments within SQL scripts. */ public static final String DEFAULT_BLOCK_COMMENT_START_DELIMITER = "/*"; /** * Default end delimiter for block comments within SQL scripts. */ public static final String DEFAULT_BLOCK_COMMENT_END_DELIMITER = "*/"; /** * Prevent instantiation of this utility class. */ private ScriptUtils() { /* no-op */ } /** * Split an SQL script into separate statements delimited by the provided * separator string. Each individual statement will be added to the provided * {@code List}. *

Within the script, the provided {@code commentPrefix} will be honored: * any text beginning with the comment prefix and extending to the end of the * line will be omitted from the output. Similarly, the provided * {@code blockCommentStartDelimiter} and {@code blockCommentEndDelimiter} * delimiters will be honored: any text enclosed in a block comment will be * omitted from the output. In addition, multiple adjacent whitespace characters * will be collapsed into a single space. * @param resource the resource from which the script was read * @param script the SQL script; never {@code null} or empty * @param separator text separating each statement — typically a ';' or * newline character; never {@code null} * @param commentPrefix the prefix that identifies SQL line comments — * typically "--"; never {@code null} or empty * @param blockCommentStartDelimiter the start block comment delimiter; * never {@code null} or empty * @param blockCommentEndDelimiter the end block comment delimiter; * never {@code null} or empty * @param statements the list that will contain the individual statements */ public static void splitSqlScript( String resource, String script, String separator, String commentPrefix, String blockCommentStartDelimiter, String blockCommentEndDelimiter, List statements ) { checkArgument(StringUtils.isNotEmpty(script), "script must not be null or empty"); checkArgument(separator != null, "separator must not be null"); checkArgument(StringUtils.isNotEmpty(commentPrefix), "commentPrefix must not be null or empty"); checkArgument( StringUtils.isNotEmpty(blockCommentStartDelimiter), "blockCommentStartDelimiter must not be null or empty" ); checkArgument( StringUtils.isNotEmpty(blockCommentEndDelimiter), "blockCommentEndDelimiter must not be null or empty" ); new ScriptSplitter( new ScriptScanner( resource, script, separator, commentPrefix, blockCommentStartDelimiter, blockCommentEndDelimiter ), statements ) .split(); } private static void checkArgument(boolean expression, String errorMessage) { if (!expression) { throw new IllegalArgumentException(errorMessage); } } /** * Does the provided SQL script contain the specified delimiter? * @param script the SQL script * @param delim String delimiting each statement - typically a ';' character */ public static boolean containsSqlScriptDelimiters(String script, String delim) { return containsSqlScriptDelimiters( "", script, DEFAULT_COMMENT_PREFIX, delim, DEFAULT_BLOCK_COMMENT_START_DELIMITER, DEFAULT_BLOCK_COMMENT_END_DELIMITER ); } /** * Does the provided SQL script contain the specified delimiter? * * @param script the SQL script * @param delim String delimiting each statement - typically a ';' character * @param commentPrefix the prefix that identifies comments in the SQL script, * typically "--" * @param blockCommentStartDelimiter block comment start delimiter * @param blockCommentEndDelimiter block comment end delimiter */ public static boolean containsSqlScriptDelimiters( String scriptPath, String script, String commentPrefix, String delim, String blockCommentStartDelimiter, String blockCommentEndDelimiter ) { ScriptScanner scanner = new ScriptScanner( scriptPath, script, delim, commentPrefix, blockCommentStartDelimiter, blockCommentEndDelimiter ); ScriptScanner.Lexem l; while ((l = scanner.next()) != ScriptScanner.Lexem.EOF) { if (ScriptScanner.Lexem.SEPARATOR.equals(l)) { return true; } } return false; } /** * Load script from classpath and apply it to the given database * * @param databaseDelegate database delegate for script execution * @param initScriptPath the resource to load the init script from */ public static void runInitScript(DatabaseDelegate databaseDelegate, String initScriptPath) { try { URL resource = Thread.currentThread().getContextClassLoader().getResource(initScriptPath); if (resource == null) { resource = ScriptUtils.class.getClassLoader().getResource(initScriptPath); if (resource == null) { LOGGER.warn("Could not load classpath init script: {}", initScriptPath); throw new ScriptLoadException( "Could not load classpath init script: " + initScriptPath + ". Resource not found." ); } } String scripts = IOUtils.toString(resource, StandardCharsets.UTF_8); executeDatabaseScript(databaseDelegate, initScriptPath, scripts); } catch (IOException e) { LOGGER.warn("Could not load classpath init script: {}", initScriptPath); throw new ScriptLoadException("Could not load classpath init script: " + initScriptPath, e); } catch (ScriptException e) { LOGGER.error("Error while executing init script: {}", initScriptPath, e); throw new UncategorizedScriptException("Error while executing init script: " + initScriptPath, e); } } public static void executeDatabaseScript(DatabaseDelegate databaseDelegate, String scriptPath, String script) throws ScriptException { executeDatabaseScript( databaseDelegate, scriptPath, script, false, false, DEFAULT_COMMENT_PREFIX, DEFAULT_STATEMENT_SEPARATOR, DEFAULT_BLOCK_COMMENT_START_DELIMITER, DEFAULT_BLOCK_COMMENT_END_DELIMITER ); } /** * Execute the given database script. *

Statement separators and comments will be removed before executing * individual statements within the supplied script. *

Do not use this method to execute DDL if you expect rollback. * @param databaseDelegate database delegate for script execution * @param scriptPath the resource (potentially associated with a specific encoding) * to load the SQL script from * @param script the raw script content *@param continueOnError whether or not to continue without throwing an exception * in the event of an error * @param ignoreFailedDrops whether or not to continue in the event of specifically * an error on a {@code DROP} statement * @param commentPrefix the prefix that identifies comments in the SQL script — * typically "--" * @param separator the script statement separator; defaults to * {@value #DEFAULT_STATEMENT_SEPARATOR} if not specified and falls back to * {@value #FALLBACK_STATEMENT_SEPARATOR} as a last resort * @param blockCommentStartDelimiter the start block comment delimiter; never * {@code null} or empty * @param blockCommentEndDelimiter the end block comment delimiter; never * {@code null} or empty @throws ScriptException if an error occurred while executing the SQL script */ public static void executeDatabaseScript( DatabaseDelegate databaseDelegate, String scriptPath, String script, boolean continueOnError, boolean ignoreFailedDrops, String commentPrefix, String separator, String blockCommentStartDelimiter, String blockCommentEndDelimiter ) throws ScriptException { try { if (LOGGER.isInfoEnabled()) { LOGGER.info("Executing database script from " + scriptPath); } long startTime = System.nanoTime(); List statements = new LinkedList<>(); if (separator == null) { separator = DEFAULT_STATEMENT_SEPARATOR; } if ( !containsSqlScriptDelimiters( scriptPath, script, commentPrefix, separator, blockCommentStartDelimiter, blockCommentEndDelimiter ) ) { separator = FALLBACK_STATEMENT_SEPARATOR; } splitSqlScript( scriptPath, script, separator, commentPrefix, blockCommentStartDelimiter, blockCommentEndDelimiter, statements ); try (DatabaseDelegate closeableDelegate = databaseDelegate) { closeableDelegate.execute(statements, scriptPath, continueOnError, ignoreFailedDrops); } long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime); if (LOGGER.isInfoEnabled()) { LOGGER.info("Executed database script from " + scriptPath + " in " + elapsedTime + " ms."); } } catch (Exception ex) { if (ex instanceof ScriptException) { throw (ScriptException) ex; } throw new UncategorizedScriptException( "Failed to execute database script from resource [" + script + "]", ex ); } } public static class ScriptLoadException extends RuntimeException { public ScriptLoadException(String message) { super(message); } public ScriptLoadException(String message, Throwable cause) { super(message, cause); } } public static class ScriptParseException extends RuntimeException { public ScriptParseException(String format, String scriptPath) { super(String.format(format, scriptPath)); } } public static class ScriptStatementFailedException extends RuntimeException { public ScriptStatementFailedException(String statement, int lineNumber, String scriptPath) { this(statement, lineNumber, scriptPath, null); } public ScriptStatementFailedException(String statement, int lineNumber, String scriptPath, Exception ex) { super(String.format("Script execution failed (%s:%d): %s", scriptPath, lineNumber, statement), ex); } } public static class UncategorizedScriptException extends RuntimeException { public UncategorizedScriptException(String s, Exception ex) { super(s, ex); } } } ================================================ FILE: modules/database-commons/src/test/java/org/testcontainers/ext/ScriptScannerTest.java ================================================ package org.testcontainers.ext; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.Test; import java.util.regex.Pattern; import static org.assertj.core.api.Assertions.assertThat; class ScriptScannerTest { @Test void testHugeStringLiteral() { String script = "/* a comment */ \"" + StringUtils.repeat('~', 10000) + "\";"; ScriptScanner scanner = scanner(script); assertThat(scanner.next()).isEqualTo(ScriptScanner.Lexem.COMMENT); assertThat(scanner.next()).isEqualTo(ScriptScanner.Lexem.WHITESPACE); assertThat(scanner.next()).isEqualTo(ScriptScanner.Lexem.QUOTED_STRING); assertThat(scanner.getCurrentMatch()).matches(Pattern.compile("\"~+\"")); } @Test void testPgIdentifierWithDollarSigns() { ScriptScanner scanner = scanner( "this$is$a$valid$postgreSQL$identifier " + "$a$While this is a quoted string$a$$ --just followed by a dollar sign" ); assertThat(scanner.next()).isEqualTo(ScriptScanner.Lexem.IDENTIFIER); assertThat(scanner.next()).isEqualTo(ScriptScanner.Lexem.WHITESPACE); assertThat(scanner.next()).isEqualTo(ScriptScanner.Lexem.QUOTED_STRING); assertThat(scanner.next()).isEqualTo(ScriptScanner.Lexem.OTHER); } @Test void testQuotedLiterals() { ScriptScanner scanner = scanner("'this \\'is a literal' \"this \\\" is a literal\""); assertThat(scanner.next()).isEqualTo(ScriptScanner.Lexem.QUOTED_STRING); assertThat(scanner.getCurrentMatch()).isEqualTo("'this \\'is a literal'"); assertThat(scanner.next()).isEqualTo(ScriptScanner.Lexem.WHITESPACE); assertThat(scanner.next()).isEqualTo(ScriptScanner.Lexem.QUOTED_STRING); assertThat(scanner.getCurrentMatch()).isEqualTo("\"this \\\" is a literal\""); } private static ScriptScanner scanner(String script) { return new ScriptScanner( "dummy", script, ScriptUtils.DEFAULT_STATEMENT_SEPARATOR, ScriptUtils.DEFAULT_COMMENT_PREFIX, ScriptUtils.DEFAULT_BLOCK_COMMENT_START_DELIMITER, ScriptUtils.DEFAULT_BLOCK_COMMENT_END_DELIMITER ); } } ================================================ FILE: modules/database-commons/src/test/java/org/testcontainers/ext/ScriptSplittingTest.java ================================================ package org.testcontainers.ext; import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class ScriptSplittingTest { @Test void testStringDemarcation() { String script = "SELECT 'foo `bar`'; SELECT 'foo -- `bar`'; SELECT 'foo /* `bar`';"; List expected = Arrays.asList("SELECT 'foo `bar`'", "SELECT 'foo -- `bar`'", "SELECT 'foo /* `bar`'"); splitAndCompare(script, expected); } @Test void testIssue1547Case1() { String script = "create database if not exists ttt;\n" + "\n" + "use ttt;\n" + "\n" + "create table aaa\n" + "(\n" + " id bigint auto_increment primary key,\n" + " end_time datetime null COMMENT 'end_time',\n" + " data_status varchar(16) not null\n" + ") comment 'aaa';\n" + "\n" + "create table bbb\n" + "(\n" + " id bigint auto_increment primary key\n" + ") comment 'bbb';"; List expected = Arrays.asList( "create database if not exists ttt", "use ttt", "create table aaa ( id bigint auto_increment primary key, end_time datetime null COMMENT 'end_time', data_status varchar(16) not null ) comment 'aaa'", "create table bbb ( id bigint auto_increment primary key ) comment 'bbb'" ); splitAndCompare(script, expected); } @Test void testIssue1547Case2() { String script = "CREATE TABLE bar (\n" + " end_time VARCHAR(255)\n" + ");\n" + "CREATE TABLE bar (\n" + " end_time VARCHAR(255)\n" + ");"; List expected = Arrays.asList( "CREATE TABLE bar ( end_time VARCHAR(255) )", "CREATE TABLE bar ( end_time VARCHAR(255) )" ); splitAndCompare(script, expected); } @Test void testSplittingEnquotedSemicolon() { String script = "CREATE TABLE `bar;bar` (\n" + " end_time VARCHAR(255)\n" + ");"; List expected = Arrays.asList("CREATE TABLE `bar;bar` ( end_time VARCHAR(255) )"); splitAndCompare(script, expected); } @Test void testUnusualSemicolonPlacement() { String script = "SELECT 1;;;;;SELECT 2;\n;SELECT 3\n; SELECT 4;\n SELECT 5"; List expected = Arrays.asList("SELECT 1", "SELECT 2", "SELECT 3", "SELECT 4", "SELECT 5"); splitAndCompare(script, expected); } @Test void testCommentedSemicolon() { String script = "CREATE TABLE bar (\n" + " foo VARCHAR(255)\n" + "); \nDROP PROCEDURE IF EXISTS -- ;\n" + " count_foo"; List expected = Arrays.asList( "CREATE TABLE bar ( foo VARCHAR(255) )", "DROP PROCEDURE IF EXISTS count_foo" ); splitAndCompare(script, expected); } @Test void testStringEscaping() { String script = "SELECT \"a /* string literal containing comment characters like -- here\";\n" + "SELECT \"a 'quoting' \\\"scenario ` involving BEGIN keyword\\\" here\";\n" + "SELECT * from `bar`;"; List expected = Arrays.asList( "SELECT \"a /* string literal containing comment characters like -- here\"", "SELECT \"a 'quoting' \\\"scenario ` involving BEGIN keyword\\\" here\"", "SELECT * from `bar`" ); splitAndCompare(script, expected); } @Test void testBlockCommentExclusion() { String script = "INSERT INTO bar (foo) /* ; */ VALUES ('hello world');"; List expected = Arrays.asList("INSERT INTO bar (foo) VALUES ('hello world')"); splitAndCompare(script, expected); } @Test void testBeginEndKeywordCorrectDetection() { String script = "INSERT INTO something_end (begin_with_the_token, another_field) /*end*/ VALUES /* end */ (' begin ', `end`)-- begin\n;"; List expected = Arrays.asList( "INSERT INTO something_end (begin_with_the_token, another_field) VALUES (' begin ', `end`)" ); splitAndCompare(script, expected); } @Test void testCommentInStrings() { String script = "CREATE TABLE bar (foo VARCHAR(255));\n" + "\n" + "/* Insert Values */\n" + "INSERT INTO bar (foo) values ('--1');\n" + "INSERT INTO bar (foo) values ('--2');\n" + "INSERT INTO bar (foo) values ('/* something */');\n" + "/* INSERT INTO bar (foo) values (' */'); -- '*/;\n" + // purposefully broken, to see if it breaks our splitting "INSERT INTO bar (foo) values ('foo');"; List expected = Arrays.asList( "CREATE TABLE bar (foo VARCHAR(255))", "INSERT INTO bar (foo) values ('--1')", "INSERT INTO bar (foo) values ('--2')", "INSERT INTO bar (foo) values ('/* something */')", "'); -- '*/", "INSERT INTO bar (foo) values ('foo')" ); splitAndCompare(script, expected); } @Test void testMultipleBeginEndDetection() { String script = "CREATE TABLE bar (foo VARCHAR(255));\n" + "\n" + "CREATE TABLE gender (gender VARCHAR(255));\n" + "CREATE TABLE ending (ending VARCHAR(255));\n" + "CREATE TABLE end2 (end2 VARCHAR(255));\n" + "CREATE TABLE end_2 (end2 VARCHAR(255));\n" + "\n" + "BEGIN\n" + " INSERT INTO ending values ('ending');\n" + "END;\n" + "\n" + "BEGIN\n" + " INSERT INTO ending values ('ending');\n" + "END/*hello*/;\n" + "\n" + "BEGIN--Hello\n" + " INSERT INTO ending values ('ending');\n" + "END;\n" + "\n" + "/*Hello*/BEGIN\n" + " INSERT INTO ending values ('ending');\n" + "END;\n" + "\n" + "CREATE TABLE foo (bar VARCHAR(255));"; List expected = Arrays.asList( "CREATE TABLE bar (foo VARCHAR(255))", "CREATE TABLE gender (gender VARCHAR(255))", "CREATE TABLE ending (ending VARCHAR(255))", "CREATE TABLE end2 (end2 VARCHAR(255))", "CREATE TABLE end_2 (end2 VARCHAR(255))", "BEGIN\n" + " INSERT INTO ending values ('ending');\n" + "END", "BEGIN\n" + " INSERT INTO ending values ('ending');\n" + "END", "BEGIN--Hello\n" + " INSERT INTO ending values ('ending');\n" + "END", "BEGIN\n" + " INSERT INTO ending values ('ending');\n" + "END", "CREATE TABLE foo (bar VARCHAR(255))" ); splitAndCompare(script, expected); } @Test void testProcedureBlock() { String script = "CREATE PROCEDURE count_foo()\n" + " BEGIN\n" + "\n" + " BEGIN\n" + " SELECT *\n" + " FROM bar;\n" + " SELECT 1\n" + " FROM dual;\n" + " END;\n" + "\n" + " BEGIN\n" + " select * from bar;\n" + " END;\n" + "\n" + " -- we can do comments\n" + "\n" + " /* including block\n" + " comments\n" + " */\n" + "\n" + " /* what if BEGIN appears inside a comment? */\n" + "\n" + " select \"or what if BEGIN appears inside a literal?\";\n" + "\n" + " END /*; */;"; List expected = Arrays.asList( "CREATE PROCEDURE count_foo() BEGIN\n" + "\n" + " BEGIN\n" + " SELECT *\n" + " FROM bar;\n" + " SELECT 1\n" + " FROM dual;\n" + " END;\n" + "\n" + " BEGIN\n" + " select * from bar;\n" + " END;\n" + "\n" + " -- we can do comments\n" + "\n" + " /* including block\n" + " comments\n" + " */\n" + "\n" + " /* what if BEGIN appears inside a comment? */\n" + "\n" + " select \"or what if BEGIN appears inside a literal?\";\n" + "\n" + " END" ); splitAndCompare(script, expected); } @Test void testUnclosedBlockComment() { String script = "SELECT 'foo `bar`'; /*"; assertThatThrownBy(() -> doSplit(script, ScriptUtils.DEFAULT_STATEMENT_SEPARATOR)) .isInstanceOf(ScriptUtils.ScriptParseException.class) .hasMessageContaining("*/"); } @Test void testIssue1452Case() { String script = "create table test (text VARCHAR(255));\n" + "\n" + "/* some comment */\n" + "insert into `test` (`text`) values ('a b');"; List expected = Arrays.asList( "create table test (text VARCHAR(255))", "insert into `test` (`text`) values ('a b')" ); splitAndCompare(script, expected); } @Test void testIfLoopBlocks() { String script = "BEGIN\n" + " rec_loop: LOOP\n" + " FETCH blah;\n" + " IF something_wrong THEN LEAVE rec_loop; END IF;\n" + " do_something_else;\n" + " END LOOP;\n" + "END /* final comment */;"; List expected = Collections.singletonList( "BEGIN\n" + " rec_loop: LOOP\n" + " FETCH blah;\n" + " IF something_wrong THEN LEAVE rec_loop; END IF;\n" + " do_something_else;\n" + " END LOOP;\n" + "END" ); splitAndCompare(script, expected); } @Test void testIfLoopBlocksSpecificSeparator() { String script = "BEGIN\n" + " rec_loop: LOOP\n" + " FETCH blah;\n" + " IF something_wrong THEN LEAVE rec_loop; END IF;\n" + " do_something_else;\n" + " END LOOP;\n" + "END;\n" + "@\n" + "CALL something();\n" + "@\n"; List expected = Arrays.asList( "BEGIN\n" + " rec_loop: LOOP\n" + " FETCH blah;\n" + " IF something_wrong THEN LEAVE rec_loop; END IF;\n" + " do_something_else;\n" + " END LOOP;\n" + "END;", "CALL something();" ); splitAndCompare(script, expected, "@"); } @Test void oracleStyleBlocks() { String script = "BEGIN END; /\n" + "BEGIN END;"; List expected = Arrays.asList("BEGIN END;", "BEGIN END;"); splitAndCompare(script, expected, "/"); } @Test void testMultiProcedureMySQLScript() { String script = "CREATE PROCEDURE doiterate(p1 INT)\n" + " BEGIN\n" + " label1: LOOP\n" + " SET p1 = p1 + 1;\n" + " IF p1 < 10 THEN\n" + " ITERATE label1;\n" + " END IF;\n" + " LEAVE label1;\n" + " END LOOP label1;\n" + " END;\n" + "\n" + "CREATE PROCEDURE dowhile()\n" + " BEGIN\n" + " DECLARE v1 INT DEFAULT 5;\n" + " WHILE v1 > 0 DO\n" + " SET v1 = v1 - 1;\n" + " END WHILE;\n" + " END;\n" + "\n" + "CREATE PROCEDURE dorepeat(p1 INT)\n" + " BEGIN\n" + " SET @x = 0;\n" + " REPEAT\n" + " SET @x = @x + 1;\n" + " UNTIL @x > p1 END REPEAT;\n" + " END;"; List expected = Arrays.asList( "CREATE PROCEDURE doiterate(p1 INT) BEGIN\n" + " label1: LOOP\n" + " SET p1 = p1 + 1;\n" + " IF p1 < 10 THEN\n" + " ITERATE label1;\n" + " END IF;\n" + " LEAVE label1;\n" + " END LOOP label1;\n" + " END", "CREATE PROCEDURE dowhile() BEGIN\n" + " DECLARE v1 INT DEFAULT 5;\n" + " WHILE v1 > 0 DO\n" + " SET v1 = v1 - 1;\n" + " END WHILE;\n" + " END", "CREATE PROCEDURE dorepeat(p1 INT) BEGIN\n" + " SET @x = 0;\n" + " REPEAT\n" + " SET @x = @x + 1;\n" + " UNTIL @x > p1 END REPEAT;\n" + " END" ); splitAndCompare(script, expected); } @Test void testDollarQuotedStrings() { String script = "CREATE FUNCTION f ()\n" + "RETURNS INT\n" + "AS $$\n" + "BEGIN\n" + " RETURN 1;\n" + "END;\n" + "$$ LANGUAGE plpgsql;"; List expected = Collections.singletonList( "CREATE FUNCTION f () RETURNS INT AS $$\n" + "BEGIN\n" + " RETURN 1;\n" + "END;\n" + "$$ LANGUAGE plpgsql" ); splitAndCompare(script, expected); } @Test void testNestedDollarQuotedString() { //see https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-DOLLAR-QUOTING String script = "CREATE FUNCTION f() AS $function$\n" + "BEGIN\n" + " RETURN ($1 ~ $q$[\\t\\r\\n\\v\\\\]$q$);\n" + "END;\n" + "$function$;" + "create table foo ();"; List expected = Arrays.asList( "CREATE FUNCTION f() AS $function$\n" + "BEGIN\n" + " RETURN ($1 ~ $q$[\\t\\r\\n\\v\\\\]$q$);\n" + "END;\n" + "$function$", "create table foo ()" ); splitAndCompare(script, expected); } @Test void testUnclosedDollarQuotedString() { String script = "SELECT $tag$ ..... $"; assertThatThrownBy(() -> doSplit(script, ScriptUtils.DEFAULT_STATEMENT_SEPARATOR)) .isInstanceOf(ScriptUtils.ScriptParseException.class) .hasMessageContaining("$tag$"); } private void splitAndCompare(String script, List expected) { splitAndCompare(script, expected, ScriptUtils.DEFAULT_STATEMENT_SEPARATOR); } private void splitAndCompare(String script, List expected, String separator) { final List statements = doSplit(script, separator); assertThat(statements).isEqualTo(expected); } private List doSplit(String script, String separator) { final List statements = new ArrayList<>(); ScriptUtils.splitSqlScript( "ignored", script, separator, ScriptUtils.DEFAULT_COMMENT_PREFIX, ScriptUtils.DEFAULT_BLOCK_COMMENT_START_DELIMITER, ScriptUtils.DEFAULT_BLOCK_COMMENT_END_DELIMITER, statements ); return statements; } @Test void testIgnoreDelimitersInLiteralsAndComments() { assertThat(ScriptUtils.containsSqlScriptDelimiters("'@' /*@*/ \"@\" $tag$@$tag$ --@", "@")).isFalse(); } @Test void testContainsDelimiters() { assertThat(ScriptUtils.containsSqlScriptDelimiters("'@' /*@*/ @ \"@\" --@", "@")).isTrue(); } } ================================================ FILE: modules/databend/build.gradle ================================================ description = "Testcontainers :: JDBC :: Databend" dependencies { api project(':testcontainers-jdbc') testImplementation project(':testcontainers-jdbc-test') testRuntimeOnly 'com.databend:databend-jdbc:0.4.1' } ================================================ FILE: modules/databend/src/main/java/org/testcontainers/databend/DatabendContainer.java ================================================ package org.testcontainers.databend; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import java.util.HashSet; import java.util.Set; /** * Testcontainers implementation for Databend. *

* Supported image: {@code datafuselabs/databend} *

* Exposed ports: *

    *
  • Database: 8000
  • *
*/ public class DatabendContainer extends JdbcDatabaseContainer { static final String NAME = "databend"; static final DockerImageName DOCKER_IMAGE_NAME = DockerImageName.parse("datafuselabs/databend"); private static final Integer HTTP_PORT = 8000; private static final String DRIVER_CLASS_NAME = "com.databend.jdbc.DatabendDriver"; private static final String JDBC_URL_PREFIX = "jdbc:" + NAME + "://"; private static final String TEST_QUERY = "SELECT 1"; private String databaseName = "default"; private String username = "databend"; private String password = "databend"; public DatabendContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public DatabendContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DOCKER_IMAGE_NAME); addExposedPorts(HTTP_PORT); waitingFor(Wait.forHttp("/").forResponsePredicate(response -> response.equals("Ok."))); } @Override protected void configure() { withEnv("QUERY_DEFAULT_USER", this.username); withEnv("QUERY_DEFAULT_PASSWORD", this.password); } @Override public Set getLivenessCheckPortNumbers() { return new HashSet<>(getMappedPort(HTTP_PORT)); } @Override public String getDriverClassName() { return DRIVER_CLASS_NAME; } @Override public String getJdbcUrl() { return ( JDBC_URL_PREFIX + getHost() + ":" + getMappedPort(HTTP_PORT) + "/" + this.databaseName + constructUrlParameters("?", "&") ); } @Override public String getUsername() { return this.username; } @Override public String getPassword() { return this.password; } @Override public String getDatabaseName() { return this.databaseName; } @Override public String getTestQueryString() { return TEST_QUERY; } @Override public DatabendContainer withUsername(String username) { this.username = username; return this; } @Override public DatabendContainer withPassword(String password) { this.password = password; return this; } } ================================================ FILE: modules/databend/src/main/java/org/testcontainers/databend/DatabendContainerProvider.java ================================================ package org.testcontainers.databend; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.containers.JdbcDatabaseContainerProvider; public class DatabendContainerProvider extends JdbcDatabaseContainerProvider { private static final String DEFAULT_TAG = "v1.2.615"; @Override public boolean supports(String databaseType) { return databaseType.equals(DatabendContainer.NAME); } @Override public JdbcDatabaseContainer newInstance() { return newInstance(DEFAULT_TAG); } @Override public JdbcDatabaseContainer newInstance(String tag) { if (tag != null) { return new DatabendContainer(DatabendContainer.DOCKER_IMAGE_NAME.withTag(tag)); } else { return newInstance(); } } } ================================================ FILE: modules/databend/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider ================================================ org.testcontainers.databend.DatabendContainerProvider ================================================ FILE: modules/databend/src/test/java/org/testcontainers/databend/DatabendContainerTest.java ================================================ package org.testcontainers.databend; import org.junit.jupiter.api.Test; import org.testcontainers.db.AbstractContainerDatabaseTest; import java.sql.ResultSet; import java.sql.SQLException; import static org.assertj.core.api.Assertions.assertThat; class DatabendContainerTest extends AbstractContainerDatabaseTest { @Test void testSimple() throws SQLException { try ( // container { DatabendContainer databend = new DatabendContainer("datafuselabs/databend:v1.2.615") // } ) { databend.start(); ResultSet resultSet = performQuery(databend, "SELECT 1"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).isEqualTo(1); } } @Test void customCredentialsWithUrlParams() throws SQLException { try ( DatabendContainer databend = new DatabendContainer("datafuselabs/databend:v1.2.615") .withUsername("test") .withPassword("test") .withUrlParam("ssl", "false") ) { databend.start(); ResultSet resultSet = performQuery(databend, "SELECT 1;"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).isEqualTo(1); } } } ================================================ FILE: modules/databend/src/test/java/org/testcontainers/databend/DatabendJDBCDriverTest.java ================================================ package org.testcontainers.databend; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; class DatabendJDBCDriverTest extends AbstractJDBCDriverTest { public static Iterable data() { return Arrays.asList( new Object[][] { // { "jdbc:tc:databend://hostname/databasename", EnumSet.of(Options.PmdKnownBroken) }, } ); } } ================================================ FILE: modules/databend/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/db2/build.gradle ================================================ description = "Testcontainers :: JDBC :: DB2" dependencies { api project(':testcontainers-jdbc') testImplementation project(':testcontainers-jdbc-test') testRuntimeOnly 'com.ibm.db2:jcc:12.1.3.0' } ================================================ FILE: modules/db2/src/main/java/org/testcontainers/containers/Db2Container.java ================================================ 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; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.Set; /** * Testcontainers implementation for IBM DB2. *

* Supported images: {@code icr.io/db2_community/db2}, {@code ibmcom/db2} *

* Exposed ports: *

    *
  • Database: 50000
  • *
* @deprecated use {@link org.testcontainers.db2.Db2Container} instead. */ @Deprecated public class Db2Container extends JdbcDatabaseContainer { public static final String NAME = "db2"; @Deprecated private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("ibmcom/db2"); private static final DockerImageName DEFAULT_NEW_IMAGE_NAME = DockerImageName.parse("icr.io/db2_community/db2"); @Deprecated public static final String DEFAULT_DB2_IMAGE_NAME = DEFAULT_IMAGE_NAME.getUnversionedPart(); @Deprecated public static final String DEFAULT_TAG = "11.5.0.0a"; public static final int DB2_PORT = 50000; private String databaseName = "test"; private String username = "db2inst1"; private String password = "foobar1234"; /** * @deprecated use {@link #Db2Container(DockerImageName)} instead */ @Deprecated public Db2Container() { this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } public Db2Container(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public Db2Container(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_NEW_IMAGE_NAME, DEFAULT_IMAGE_NAME); withCreateContainerCmdModifier(cmd -> cmd.withCapAdd(Capability.IPC_LOCK).withCapAdd(Capability.IPC_OWNER)); this.waitStrategy = new LogMessageWaitStrategy() .withRegEx(".*Setup has completed\\..*") .withStartupTimeout(Duration.of(10, ChronoUnit.MINUTES)); addExposedPort(DB2_PORT); } /** * @return the ports on which to check if the container is ready * @deprecated use {@link #getLivenessCheckPortNumbers()} instead */ @Override @Deprecated protected Set getLivenessCheckPorts() { return super.getLivenessCheckPorts(); } @Override protected void configure() { // If license was not accepted programmatically, check if it was accepted via resource file if (!getEnvMap().containsKey("LICENSE")) { LicenseAcceptance.assertLicenseAccepted(this.getDockerImageName()); acceptLicense(); } addEnv("DBNAME", databaseName); addEnv("DB2INSTANCE", username); addEnv("DB2INST1_PASSWORD", password); // These settings help the DB2 container start faster if (!getEnvMap().containsKey("AUTOCONFIG")) { addEnv("AUTOCONFIG", "false"); } if (!getEnvMap().containsKey("ARCHIVE_LOGS")) { addEnv("ARCHIVE_LOGS", "false"); } } /** * Accepts the license for the DB2 container by setting the LICENSE=accept * variable as described at https://hub.docker.com/r/ibmcom/db2 */ public Db2Container acceptLicense() { addEnv("LICENSE", "accept"); return this; } @Override public String getDriverClassName() { return "com.ibm.db2.jcc.DB2Driver"; } @Override public String getJdbcUrl() { String additionalUrlParams = constructUrlParameters(":", ";", ";"); return "jdbc:db2://" + getHost() + ":" + getMappedPort(DB2_PORT) + "/" + databaseName + additionalUrlParams; } @Override public String getUsername() { return username; } @Override public String getPassword() { return password; } @Override public String getDatabaseName() { return databaseName; } @Override public Db2Container withUsername(String username) { this.username = username; return this; } @Override public Db2Container withPassword(String password) { this.password = password; return this; } @Override public Db2Container withDatabaseName(String dbName) { this.databaseName = dbName; return this; } @Override protected void waitUntilContainerStarted() { getWaitStrategy().waitUntilReady(this); } @Override protected String getTestQueryString() { return "SELECT 1 FROM SYSIBM.SYSDUMMY1"; } } ================================================ FILE: modules/db2/src/main/java/org/testcontainers/containers/Db2ContainerProvider.java ================================================ package org.testcontainers.containers; import org.testcontainers.utility.DockerImageName; public class Db2ContainerProvider extends JdbcDatabaseContainerProvider { @Override public boolean supports(String databaseType) { return databaseType.equals(Db2Container.NAME); } @Override public JdbcDatabaseContainer newInstance() { return newInstance(Db2Container.DEFAULT_TAG); } @Override public JdbcDatabaseContainer newInstance(String tag) { return new Db2Container(DockerImageName.parse(Db2Container.DEFAULT_DB2_IMAGE_NAME).withTag(tag)); } } ================================================ FILE: modules/db2/src/main/java/org/testcontainers/db2/Db2Container.java ================================================ package org.testcontainers.db2; import com.github.dockerjava.api.model.Capability; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.LicenseAcceptance; import java.time.Duration; import java.util.Set; /** * Testcontainers implementation for IBM DB2. *

* Supported images: {@code icr.io/db2_community/db2}, {@code ibmcom/db2} *

* Exposed ports: *

    *
  • Database: 50000
  • *
*/ public class Db2Container extends JdbcDatabaseContainer { public static final String NAME = "db2"; private static final DockerImageName DEFAULT_NEW_IMAGE_NAME = DockerImageName.parse("icr.io/db2_community/db2"); public static final int DB2_PORT = 50000; private String databaseName = "test"; private String username = "db2inst1"; private String password = "foobar1234"; public Db2Container(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public Db2Container(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_NEW_IMAGE_NAME); withCreateContainerCmdModifier(cmd -> cmd.withCapAdd(Capability.IPC_LOCK).withCapAdd(Capability.IPC_OWNER)); waitingFor(Wait.forLogMessage(".*Setup has completed\\..*", 1).withStartupTimeout(Duration.ofMinutes(10))); addExposedPort(DB2_PORT); } /** * @return the ports on which to check if the container is ready * @deprecated use {@link #getLivenessCheckPortNumbers()} instead */ @Override @Deprecated protected Set getLivenessCheckPorts() { return super.getLivenessCheckPorts(); } @Override protected void configure() { // If license was not accepted programmatically, check if it was accepted via resource file if (!getEnvMap().containsKey("LICENSE")) { LicenseAcceptance.assertLicenseAccepted(this.getDockerImageName()); acceptLicense(); } addEnv("DBNAME", databaseName); addEnv("DB2INSTANCE", username); addEnv("DB2INST1_PASSWORD", password); // These settings help the DB2 container start faster if (!getEnvMap().containsKey("AUTOCONFIG")) { addEnv("AUTOCONFIG", "false"); } if (!getEnvMap().containsKey("ARCHIVE_LOGS")) { addEnv("ARCHIVE_LOGS", "false"); } } /** * Accepts the license for the DB2 container by setting the LICENSE=accept * variable as described at https://hub.docker.com/r/ibmcom/db2 */ public Db2Container acceptLicense() { addEnv("LICENSE", "accept"); return this; } @Override public String getDriverClassName() { return "com.ibm.db2.jcc.DB2Driver"; } @Override public String getJdbcUrl() { String additionalUrlParams = constructUrlParameters(":", ";", ";"); return "jdbc:db2://" + getHost() + ":" + getMappedPort(DB2_PORT) + "/" + databaseName + additionalUrlParams; } @Override public String getUsername() { return username; } @Override public String getPassword() { return password; } @Override public String getDatabaseName() { return databaseName; } @Override public Db2Container withUsername(String username) { this.username = username; return this; } @Override public Db2Container withPassword(String password) { this.password = password; return this; } @Override public Db2Container withDatabaseName(String dbName) { this.databaseName = dbName; return this; } @Override protected void waitUntilContainerStarted() { getWaitStrategy().waitUntilReady(this); } @Override protected String getTestQueryString() { return "SELECT 1 FROM SYSIBM.SYSDUMMY1"; } } ================================================ FILE: modules/db2/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider ================================================ org.testcontainers.containers.Db2ContainerProvider ================================================ FILE: modules/db2/src/test/java/org/testcontainers/Db2TestImages.java ================================================ package org.testcontainers; import org.testcontainers.utility.DockerImageName; public interface Db2TestImages { DockerImageName DB2_IMAGE = DockerImageName.parse("icr.io/db2_community/db2:11.5.8.0"); } ================================================ FILE: modules/db2/src/test/java/org/testcontainers/db2/Db2ContainerTest.java ================================================ package org.testcontainers.db2; import org.junit.jupiter.api.Test; import org.testcontainers.Db2TestImages; import org.testcontainers.db.AbstractContainerDatabaseTest; import java.sql.ResultSet; import java.sql.SQLException; import static org.assertj.core.api.Assertions.assertThat; class Db2ContainerTest extends AbstractContainerDatabaseTest { @Test void testSimple() throws SQLException { try ( // container { Db2Container db2 = new Db2Container("icr.io/db2_community/db2:11.5.8.0").acceptLicense() // } ) { db2.start(); ResultSet resultSet = performQuery(db2, "SELECT 1 FROM SYSIBM.SYSDUMMY1"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); assertHasCorrectExposedAndLivenessCheckPorts(db2); } } @Test void testSimpleWithNewImage() throws SQLException { try (Db2Container db2 = new Db2Container("icr.io/db2_community/db2:11.5.8.0").acceptLicense()) { db2.start(); ResultSet resultSet = performQuery(db2, "SELECT 1 FROM SYSIBM.SYSDUMMY1"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); assertHasCorrectExposedAndLivenessCheckPorts(db2); } } @Test void testWithAdditionalUrlParamInJdbcUrl() { try ( Db2Container db2 = new Db2Container(Db2TestImages.DB2_IMAGE) .withUrlParam("sslConnection", "false") .acceptLicense() ) { db2.start(); String jdbcUrl = db2.getJdbcUrl(); assertThat(jdbcUrl).contains(":sslConnection=false;"); } } private void assertHasCorrectExposedAndLivenessCheckPorts(Db2Container db2) { assertThat(db2.getExposedPorts()).containsExactly(Db2Container.DB2_PORT); assertThat(db2.getLivenessCheckPortNumbers()).containsExactly(db2.getMappedPort(Db2Container.DB2_PORT)); } } ================================================ FILE: modules/db2/src/test/java/org/testcontainers/jdbc/db2/DB2JDBCDriverTest.java ================================================ package org.testcontainers.jdbc.db2; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; class DB2JDBCDriverTest extends AbstractJDBCDriverTest { public static Iterable data() { return Arrays.asList( new Object[][] { // { "jdbc:tc:db2://hostname/databasename", EnumSet.noneOf(Options.class) }, } ); } } ================================================ FILE: modules/db2/src/test/resources/container-license-acceptance.txt ================================================ ibmcom/db2:11.5.0.0a ================================================ FILE: modules/db2/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/elasticsearch/build.gradle ================================================ description = "Testcontainers :: elasticsearch" dependencies { api project(':testcontainers') testImplementation "org.elasticsearch.client:elasticsearch-rest-client:9.2.2" testImplementation "org.elasticsearch.client:transport:7.17.29" } ================================================ FILE: modules/elasticsearch/src/main/java/org/testcontainers/elasticsearch/ElasticsearchContainer.java ================================================ package org.testcontainers.elasticsearch; import com.github.dockerjava.api.exception.NotFoundException; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.testcontainers.containers.BindMode; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.ComparableVersion; import org.testcontainers.utility.DockerImageName; import java.io.ByteArrayInputStream; import java.net.InetSocketAddress; import java.security.KeyStore; import java.security.cert.Certificate; import java.security.cert.CertificateFactory; import java.util.Optional; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; /** * Testcontainers implementation for Elasticsearch. *

* Supported image: {@code docker.elastic.co/elasticsearch/elasticsearch}, {@code elasticsearch} *

* Exposed ports: *

    *
  • HTTP: 9200
  • *
  • TCP Transport: 9300
  • *
*/ @Slf4j public class ElasticsearchContainer extends GenericContainer { /** * Elasticsearch Default Password for Elasticsearch >= 8 */ public static final String ELASTICSEARCH_DEFAULT_PASSWORD = "changeme"; /** * Elasticsearch Default HTTP port */ private static final int ELASTICSEARCH_DEFAULT_PORT = 9200; /** * Elasticsearch Default Transport port * The TransportClient will be removed in Elasticsearch 8. No need to expose this port anymore in the future. */ @Deprecated private static final int ELASTICSEARCH_DEFAULT_TCP_PORT = 9300; /** * Elasticsearch Docker base image */ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse( "docker.elastic.co/elasticsearch/elasticsearch" ); @Deprecated private static final DockerImageName DEFAULT_OSS_IMAGE_NAME = DockerImageName.parse( "docker.elastic.co/elasticsearch/elasticsearch-oss" ); private static final DockerImageName ELASTICSEARCH_IMAGE_NAME = DockerImageName.parse("elasticsearch"); // default location of the automatically generated self-signed HTTP cert for versions >= 8 private static final String DEFAULT_CERT_PATH = "/usr/share/elasticsearch/config/certs/http_ca.crt"; @Deprecated private boolean isOss = false; private final boolean isAtLeastMajorVersion8; private String certPath = ""; /** * Create an Elasticsearch Container by passing the full docker image name * * @param dockerImageName Full docker image name as a {@link String}, like: docker.elastic.co/elasticsearch/elasticsearch:7.9.2 */ public ElasticsearchContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } /** * Create an Elasticsearch Container by passing the full docker image name * * @param dockerImageName Full docker image name as a {@link DockerImageName}, like: DockerImageName.parse("docker.elastic.co/elasticsearch/elasticsearch:7.9.2") */ public ElasticsearchContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, DEFAULT_OSS_IMAGE_NAME, ELASTICSEARCH_IMAGE_NAME); if (dockerImageName.isCompatibleWith(DEFAULT_OSS_IMAGE_NAME)) { this.isOss = true; log.warn( "{} is not supported anymore after 7.10.2. Please switch to {}", dockerImageName.getUnversionedPart(), DEFAULT_IMAGE_NAME.getUnversionedPart() ); } withEnv("discovery.type", "single-node"); // disable disk threshold checks withEnv("cluster.routing.allocation.disk.threshold_enabled", "false"); // Sets default memory of elasticsearch instance to 2GB // Spaces are deliberate to allow user to define additional jvm options as elasticsearch resolves option files lexicographically withClasspathResourceMapping( "elasticsearch-default-memory-vm.options", "/usr/share/elasticsearch/config/jvm.options.d/ elasticsearch-default-memory-vm.options", BindMode.READ_ONLY ); addExposedPorts(ELASTICSEARCH_DEFAULT_PORT, ELASTICSEARCH_DEFAULT_TCP_PORT); this.isAtLeastMajorVersion8 = new ComparableVersion(dockerImageName.getVersionPart()).isGreaterThanOrEqualTo("8.0.0"); // regex that // matches 8.3 JSON logging with started message and some follow up content within the message field // matches 8.0 JSON logging with no whitespace between message field and content // matches 7.x JSON logging with whitespace between message field and content // matches 6.x text logging with node name in brackets and just a 'started' message till the end of the line String regex = ".*(\"message\":\\s?\"started[\\s?|\"].*|] started\n$)"; setWaitStrategy(Wait.forLogMessage(regex, 1)); if (isAtLeastMajorVersion8) { withPassword(ELASTICSEARCH_DEFAULT_PASSWORD); withCertPath(DEFAULT_CERT_PATH); } } /** * If this is running above Elasticsearch 8, this will return the probably self-signed CA cert that has been extracted * * @return byte array optional containing the CA cert extracted from the docker container */ public Optional caCertAsBytes() { if (StringUtils.isBlank(certPath)) { return Optional.empty(); } try { byte[] bytes = copyFileFromContainer(certPath, IOUtils::toByteArray); if (bytes.length > 0) { return Optional.of(bytes); } } catch (NotFoundException e) { // just emit an error message, but do not throw an exception // this might be ok, if the docker image is accidentally looking like version 8 or latest // can happen if Elasticsearch is repackaged, i.e. with custom plugins log.warn("CA cert under " + certPath + " not found."); } return Optional.empty(); } /** * A SSL context based on the self-signed CA, so that using this SSL Context allows to connect to the Elasticsearch service * @return a customized SSL Context */ public SSLContext createSslContextFromCa() { try { CertificateFactory factory = CertificateFactory.getInstance("X.509"); Certificate trustedCa = factory.generateCertificate( new ByteArrayInputStream( caCertAsBytes() .orElseThrow(() -> new IllegalStateException("CA cert under " + certPath + " not found.")) ) ); KeyStore trustStore = KeyStore.getInstance("pkcs12"); trustStore.load(null, null); trustStore.setCertificateEntry("ca", trustedCa); final SSLContext sslContext = SSLContext.getInstance("TLSv1.3"); TrustManagerFactory tmfactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmfactory.init(trustStore); sslContext.init(null, tmfactory.getTrustManagers(), null); return sslContext; } catch (Exception e) { throw new RuntimeException(e); } } /** * Define the Elasticsearch password to set. It enables security behind the scene for major version below 8.0.0. * It's not possible to use security with the oss image. * @param password Password to set * @return this */ public ElasticsearchContainer withPassword(String password) { if (isOss) { throw new IllegalArgumentException( "You can not activate security on Elastic OSS Image. Please switch to the default distribution" ); } withEnv("ELASTIC_PASSWORD", password); if (!isAtLeastMajorVersion8) { // major version 8 is secure by default and does not need this to enable authentication withEnv("xpack.security.enabled", "true"); } return this; } /** * Configure a CA cert path that is not the default * * @param certPath Path to the CA certificate within the Docker container to extract it from after start up * @return this */ public ElasticsearchContainer withCertPath(String certPath) { this.certPath = certPath; return this; } public String getHttpHostAddress() { return getHost() + ":" + getMappedPort(ELASTICSEARCH_DEFAULT_PORT); } // The TransportClient will be removed in Elasticsearch 8. No need to expose this port anymore in the future. @Deprecated public InetSocketAddress getTcpHost() { return new InetSocketAddress(getHost(), getMappedPort(ELASTICSEARCH_DEFAULT_TCP_PORT)); } } ================================================ FILE: modules/elasticsearch/src/main/resources/elasticsearch-default-memory-vm.options ================================================ -Xms2147483648 -Xmx2147483648 -Dingest.geoip.downloader.enabled.default=false ================================================ FILE: modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/ElasticsearchContainerTest.java ================================================ package org.testcontainers.elasticsearch; import com.github.dockerjava.api.DockerClient; import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.CredentialsProvider; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.util.EntityUtils; import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; import org.elasticsearch.client.Request; import org.elasticsearch.client.Response; import org.elasticsearch.client.ResponseException; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.transport.TransportClient; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.transport.client.PreBuiltTransportClient; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.BindMode; import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.images.RemoteDockerImage; import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.io.IOException; import javax.net.ssl.SSLHandshakeException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; class ElasticsearchContainerTest { /** * Elasticsearch version which should be used for the Tests */ private static final String ELASTICSEARCH_VERSION = "7.9.2"; private static final DockerImageName ELASTICSEARCH_IMAGE = DockerImageName .parse("docker.elastic.co/elasticsearch/elasticsearch") .withTag(ELASTICSEARCH_VERSION); /** * Elasticsearch default username, when secured */ private static final String ELASTICSEARCH_USERNAME = "elastic"; /** * From 6.8, we can optionally activate security with a default password. */ private static final String ELASTICSEARCH_PASSWORD = "changeme"; private RestClient client = null; private RestClient anonymousClient = null; @AfterEach public void stopRestClient() throws IOException { if (client != null) { client.close(); client = null; } if (anonymousClient != null) { anonymousClient.close(); anonymousClient = null; } } @SuppressWarnings("deprecation") // Using deprecated constructor for verification of backwards compatibility @Test @Deprecated // We will remove this test in the future void elasticsearchDeprecatedCtorTest() throws IOException { // Create the elasticsearch container. try ( ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE).withEnv("foo", "bar") // dummy env for compiler checking correct generics usage ) { // Start the container. This step might take some time... container.start(); // Do whatever you want with the rest client ... Response response = getClient(container).performRequest(new Request("GET", "/")); assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200); assertThat(EntityUtils.toString(response.getEntity())).contains(ELASTICSEARCH_VERSION); // The default image is running with the features under Elastic License response = getClient(container).performRequest(new Request("GET", "/_xpack/")); assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200); // For now we test that we have the monitoring feature available assertThat(EntityUtils.toString(response.getEntity())).contains("monitoring"); } } @Test void elasticsearchDefaultTest() throws IOException { // Create the elasticsearch container. try ( ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE).withEnv("foo", "bar") // dummy env for compiler checking correct generics usage ) { // Start the container. This step might take some time... container.start(); // Do whatever you want with the rest client ... Response response = getClient(container).performRequest(new Request("GET", "/")); assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200); assertThat(EntityUtils.toString(response.getEntity())).contains(ELASTICSEARCH_VERSION); // The default image is running with the features under Elastic License response = getClient(container).performRequest(new Request("GET", "/_xpack/")); assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200); // For now we test that we have the monitoring feature available assertThat(EntityUtils.toString(response.getEntity())).contains("monitoring"); } } @Test void elasticsearchSecuredTest() throws IOException { try ( ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE) .withPassword(ELASTICSEARCH_PASSWORD) ) { container.start(); // The cluster should be secured so it must fail when we try to access / without credentials assertThat(catchThrowable(() -> getAnonymousClient(container).performRequest(new Request("GET", "/")))) .as("We should not be able to access / URI with an anonymous client.") .isInstanceOf(ResponseException.class); // But it should work when we try to access / with the proper login and password Response response = getClient(container).performRequest(new Request("GET", "/")); assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200); assertThat(EntityUtils.toString(response.getEntity())).contains(ELASTICSEARCH_VERSION); } } @Test void elasticsearchVersion() throws IOException { try (ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE)) { container.start(); Response response = getClient(container).performRequest(new Request("GET", "/")); assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200); String responseAsString = EntityUtils.toString(response.getEntity()); assertThat(responseAsString).contains(ELASTICSEARCH_VERSION); } } @Test void elasticsearchVersion83() throws IOException { try ( ElasticsearchContainer container = new ElasticsearchContainer( "docker.elastic.co/elasticsearch/elasticsearch:8.3.0" ) ) { container.start(); Response response = getClient(container).performRequest(new Request("GET", "/")); assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200); assertThat(EntityUtils.toString(response.getEntity())).contains("8.3.0"); } } @Test void elasticsearchOssImage() throws IOException { try ( // ossContainer { ElasticsearchContainer container = new ElasticsearchContainer( "docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.2" ) // } ) { container.start(); Response response = getClient(container).performRequest(new Request("GET", "/")); assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200); // The OSS image does not have any feature under Elastic License assertThat(catchThrowable(() -> getClient(container).performRequest(new Request("GET", "/_xpack/")))) .as("We should not have /_xpack endpoint with an OSS License") .isInstanceOf(ResponseException.class); } } @Test void restClientClusterHealth() throws IOException { // httpClientContainer7 { // Create the elasticsearch container. try (ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE)) { // Start the container. This step might take some time... container.start(); // Do whatever you want with the rest client ... final CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials( AuthScope.ANY, new UsernamePasswordCredentials(ELASTICSEARCH_USERNAME, ELASTICSEARCH_PASSWORD) ); client = RestClient .builder(HttpHost.create(container.getHttpHostAddress())) .setHttpClientConfigCallback(httpClientBuilder -> { return httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider); }) .build(); Response response = client.performRequest(new Request("GET", "/_cluster/health")); // }} assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200); assertThat(EntityUtils.toString(response.getEntity())).contains("cluster_name"); // httpClientContainer7 {{ } // } } @Test void restClientClusterHealthElasticsearch8() throws IOException { // httpClientContainer8 { // Create the elasticsearch container. try ( ElasticsearchContainer container = new ElasticsearchContainer( "docker.elastic.co/elasticsearch/elasticsearch:8.1.2" ) ) { // Start the container. This step might take some time... container.start(); // Do whatever you want with the rest client ... final CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials( AuthScope.ANY, new UsernamePasswordCredentials(ELASTICSEARCH_USERNAME, ELASTICSEARCH_PASSWORD) ); client = RestClient // use HTTPS for Elasticsearch 8 .builder(HttpHost.create("https://" + container.getHttpHostAddress())) .setHttpClientConfigCallback(httpClientBuilder -> { httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider); // SSL is activated by default in Elasticsearch 8 httpClientBuilder.setSSLContext(container.createSslContextFromCa()); return httpClientBuilder; }) .build(); Response response = client.performRequest(new Request("GET", "/_cluster/health")); // }} assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200); assertThat(EntityUtils.toString(response.getEntity())).contains("cluster_name"); // httpClientContainer8 {{ } // } } @Test void restClientClusterHealthElasticsearch8WithoutSSL() throws IOException { // httpClientContainerNoSSL8 { // Create the elasticsearch container. try ( ElasticsearchContainer container = new ElasticsearchContainer( "docker.elastic.co/elasticsearch/elasticsearch:8.1.2" ) // disable SSL .withEnv("xpack.security.transport.ssl.enabled", "false") .withEnv("xpack.security.http.ssl.enabled", "false") ) { // Start the container. This step might take some time... container.start(); // Do whatever you want with the rest client ... final CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials( AuthScope.ANY, new UsernamePasswordCredentials(ELASTICSEARCH_USERNAME, ELASTICSEARCH_PASSWORD) ); client = RestClient .builder(HttpHost.create(container.getHttpHostAddress())) .setHttpClientConfigCallback(httpClientBuilder -> { return httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider); }) .build(); Response response = client.performRequest(new Request("GET", "/_cluster/health")); // }} assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200); assertThat(EntityUtils.toString(response.getEntity())).contains("cluster_name"); // httpClientContainerNoSSL8 {{ } // } } @Test void restClientSecuredClusterHealth() throws IOException { // httpClientSecuredContainer { // Create the elasticsearch container. try ( ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE) // With a password .withPassword(ELASTICSEARCH_PASSWORD) ) { // Start the container. This step might take some time... container.start(); // Create the secured client. final CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials( AuthScope.ANY, new UsernamePasswordCredentials(ELASTICSEARCH_USERNAME, ELASTICSEARCH_PASSWORD) ); client = RestClient .builder(HttpHost.create(container.getHttpHostAddress())) .setHttpClientConfigCallback(httpClientBuilder -> { return httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider); }) .build(); Response response = client.performRequest(new Request("GET", "/_cluster/health")); // }} assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200); assertThat(EntityUtils.toString(response.getEntity())).contains("cluster_name"); // httpClientSecuredContainer {{ } // } } @SuppressWarnings("deprecation") // The TransportClient will be removed in Elasticsearch 8. @Test void transportClientClusterHealth() { // transportClientContainer { // Create the elasticsearch container. try (ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE)) { // Start the container. This step might take some time... container.start(); // Do whatever you want with the transport client TransportAddress transportAddress = new TransportAddress(container.getTcpHost()); String expectedClusterName = "docker-cluster"; Settings settings = Settings.builder().put("cluster.name", expectedClusterName).build(); try ( TransportClient transportClient = new PreBuiltTransportClient(settings) .addTransportAddress(transportAddress) ) { ClusterHealthResponse healths = transportClient.admin().cluster().prepareHealth().get(); String clusterName = healths.getClusterName(); // }}} assertThat(clusterName).isEqualTo(expectedClusterName); // transportClientContainer {{{ } } // } } @Test void incompatibleSettingsTest() { // The OSS image can not use security feature assertThat( catchThrowable(() -> { new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.2") .withPassword("foo"); }) ) .as("We should not be able to activate security with an OSS License") .isInstanceOf(IllegalArgumentException.class); } @Test void testDockerHubElasticsearch8ImageSecureByDefault() throws Exception { try (ElasticsearchContainer container = new ElasticsearchContainer("elasticsearch:8.1.2")) { container.start(); assertClusterHealthResponse(container); } } @Test void testElasticsearch8SecureByDefaultCustomCaCertFails() throws Exception { final MountableFile mountableFile = MountableFile.forClasspathResource("http_ca.crt"); String caPath = "/tmp/http_ca.crt"; try ( ElasticsearchContainer container = new ElasticsearchContainer( "docker.elastic.co/elasticsearch/elasticsearch:8.1.2" ) .withCopyToContainer(mountableFile, caPath) .withCertPath(caPath) ) { container.start(); // this is expected, as a different cert is used for creating the SSL context assertThat(catchThrowable(() -> getClusterHealth(container))) .as( "PKIX path validation failed: java.security.cert.CertPathValidatorException: Path does not chain with any of the trust anchors" ) .isInstanceOf(SSLHandshakeException.class); } } @Test void testElasticsearch8SecureByDefaultHttpWaitStrategy() throws Exception { final HttpWaitStrategy httpsWaitStrategy = Wait .forHttps("/") .forPort(9200) .forStatusCode(200) .withBasicCredentials(ELASTICSEARCH_USERNAME, ELASTICSEARCH_PASSWORD) // trusting self-signed certificate .allowInsecure(); try ( ElasticsearchContainer container = new ElasticsearchContainer( "docker.elastic.co/elasticsearch/elasticsearch:8.1.2" ) .waitingFor(httpsWaitStrategy) ) { // Start the container. This step might take some time... container.start(); assertClusterHealthResponse(container); } } @Test void testElasticsearch8SecureByDefaultFailsSilentlyOnLatestImages() throws Exception { // this test exists for custom images by users that use the `latest` tag // even though the version might be older than version 8 // this tags an old 7.x version as :latest tagImage("docker.elastic.co/elasticsearch/elasticsearch:7.9.2", "elasticsearch-tc-older-release", "latest"); DockerImageName image = DockerImageName .parse("elasticsearch-tc-older-release:latest") .asCompatibleSubstituteFor("docker.elastic.co/elasticsearch/elasticsearch"); try (ElasticsearchContainer container = new ElasticsearchContainer(image)) { container.start(); Response response = getClient(container).performRequest(new Request("GET", "/_cluster/health")); assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200); assertThat(EntityUtils.toString(response.getEntity())).contains("cluster_name"); } } @Test void testElasticsearch7CanHaveSecurityEnabledAndUseSslContext() throws Exception { String customizedCertPath = "/usr/share/elasticsearch/config/certs/http_ca_customized.crt"; try ( ElasticsearchContainer container = new ElasticsearchContainer( "docker.elastic.co/elasticsearch/elasticsearch:7.17.15" ) .withPassword(ElasticsearchContainer.ELASTICSEARCH_DEFAULT_PASSWORD) .withEnv("xpack.security.enabled", "true") .withEnv("xpack.security.http.ssl.enabled", "true") .withEnv("xpack.security.http.ssl.key", "/usr/share/elasticsearch/config/certs/elasticsearch.key") .withEnv( "xpack.security.http.ssl.certificate", "/usr/share/elasticsearch/config/certs/elasticsearch.crt" ) .withEnv("xpack.security.http.ssl.certificate_authorities", customizedCertPath) // these lines show how certificates can be created self-made way // obviously this shouldn't be done in prod environment, where proper and officially signed keys should be present .withCopyToContainer( Transferable.of( "#!/bin/bash\n" + "mkdir -p /usr/share/elasticsearch/config/certs;" + "openssl req -x509 -newkey rsa:4096 -keyout /usr/share/elasticsearch/config/certs/elasticsearch.key -out /usr/share/elasticsearch/config/certs/elasticsearch.crt -days 365 -nodes -subj \"/CN=localhost\";" + "openssl x509 -outform der -in /usr/share/elasticsearch/config/certs/elasticsearch.crt -out " + customizedCertPath + "; chown -R elasticsearch /usr/share/elasticsearch/config/certs/", 555 ), "/usr/share/elasticsearch/generate-certs.sh" ) // because we need to generate the certificates before Elasticsearch starts, the entry command has to be tuned accordingly .withCommand( "sh", "-c", "/usr/share/elasticsearch/generate-certs.sh && /usr/local/bin/docker-entrypoint.sh" ) .withCertPath(customizedCertPath) ) { container.start(); assertClusterHealthResponse(container); } } @Test void testElasticsearchDefaultMaxHeapSize() throws Exception { long defaultHeapSize = 2147483648L; try (ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE)) { container.start(); assertElasticsearchContainerHasHeapSize(container, defaultHeapSize); } } @Test void testElasticsearchCustomMaxHeapSizeInEnvironmentVariable() throws Exception { long customHeapSize = 1574961152; try ( ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE) .withEnv("ES_JAVA_OPTS", String.format("-Xms%d -Xmx%d", customHeapSize, customHeapSize)) ) { container.start(); assertElasticsearchContainerHasHeapSize(container, customHeapSize); } } @Test void testElasticsearchCustomMaxHeapSizeInJvmOptionsFile() throws Exception { long customHeapSize = 1574961152; try ( ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE) .withClasspathResourceMapping( "test-custom-memory-jvm.options", "/usr/share/elasticsearch/config/jvm.options.d/a-user-defined-jvm.options", BindMode.READ_ONLY ); ) { container.start(); assertElasticsearchContainerHasHeapSize(container, customHeapSize); } } private void tagImage(String sourceImage, String targetImage, String targetTag) throws InterruptedException { DockerClient dockerClient = DockerClientFactory.instance().client(); dockerClient .tagImageCmd(new RemoteDockerImage(DockerImageName.parse(sourceImage)).get(), targetImage, targetTag) .exec(); } private Response getClusterHealth(ElasticsearchContainer container) throws IOException { // Create the secured client. final CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials( AuthScope.ANY, new UsernamePasswordCredentials( ELASTICSEARCH_USERNAME, ElasticsearchContainer.ELASTICSEARCH_DEFAULT_PASSWORD ) ); client = RestClient .builder(HttpHost.create("https://" + container.getHttpHostAddress())) .setHttpClientConfigCallback(httpClientBuilder -> { httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider); httpClientBuilder.setSSLContext(container.createSslContextFromCa()); return httpClientBuilder; }) .build(); return client.performRequest(new Request("GET", "/_cluster/health")); } private RestClient getClient(ElasticsearchContainer container) { if (client == null) { final CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials( AuthScope.ANY, new UsernamePasswordCredentials(ELASTICSEARCH_USERNAME, ELASTICSEARCH_PASSWORD) ); String protocol = container.caCertAsBytes().isPresent() ? "https://" : "http://"; client = RestClient .builder(HttpHost.create(protocol + container.getHttpHostAddress())) .setHttpClientConfigCallback(httpClientBuilder -> { if (container.caCertAsBytes().isPresent()) { httpClientBuilder.setSSLContext(container.createSslContextFromCa()); } return httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider); }) .build(); } return client; } private RestClient getAnonymousClient(ElasticsearchContainer container) { if (anonymousClient == null) { anonymousClient = RestClient.builder(HttpHost.create(container.getHttpHostAddress())).build(); } return anonymousClient; } private void assertElasticsearchContainerHasHeapSize(ElasticsearchContainer container, long heapSizeInBytes) throws Exception { Response response = getClient(container).performRequest(new Request("GET", "/_nodes/_all/jvm")); String responseBody = EntityUtils.toString(response.getEntity()); assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200); assertThat(responseBody).contains("\"heap_init_in_bytes\":" + heapSizeInBytes); assertThat(responseBody).contains("\"heap_max_in_bytes\":" + heapSizeInBytes); } private void assertClusterHealthResponse(ElasticsearchContainer container) throws IOException { Response response = getClusterHealth(container); assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200); assertThat(EntityUtils.toString(response.getEntity())).contains("cluster_name"); } } ================================================ FILE: modules/elasticsearch/src/test/resources/http_ca.crt ================================================ -----BEGIN CERTIFICATE----- MIIFWjCCA0KgAwIBAgIVAPVLz0Dwvzl66ZVpyJY/ntos+qQZMA0GCSqGSIb3DQEB CwUAMDwxOjA4BgNVBAMTMUVsYXN0aWNzZWFyY2ggc2VjdXJpdHkgYXV0by1jb25m aWd1cmF0aW9uIEhUVFAgQ0EwHhcNMjIwNDE0MDcyOTA4WhcNMjUwNDEzMDcyOTA4 WjA8MTowOAYDVQQDEzFFbGFzdGljc2VhcmNoIHNlY3VyaXR5IGF1dG8tY29uZmln dXJhdGlvbiBIVFRQIENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA ufHmvM04dPKNF6AulX3HrjNfcYy62pBO2oJIKhScetDzuRupv/qiXkO1gzGT3/jk 8uk57rzylDFoCgFNUyWdYQAXD+qsy5vbAytGVNCHOGSuWlNm1bbDYwZNTXjZTK6C CKIY31lbFn9a4oM+Jp1kvIr8GSMQAYDisq+yrVloDLkRs1SPImnoaXsq8epxloBf En0vo5V8PtOh+xQFpPP21pd5QohCSB+jMaxaizScWX+k7BijEaS1LCsc2w2PNvwn /bEvtW7w9w+HnzRGZW2nVlt8eji1PHMmfM5Zugn4HAxsSvdI9VRFGAeT2moNiSLN zxOWmEvQVl2MxRWiTkM1EM7CFDN40hLHtHej8UddeXTDDXyLoUjY3FmaB45rLKdE k/rszepc3lmptEMh74iUowKaYZTS5jRqT0yIDzevP5je3JP+pe4aNkx7lWMhTReQ 6fs97nd71PfJBuMcHPEA14zwSzSRb/8mNqqaQLBb5H1DpDcZtzbBxb4wFSAa7Dd0 pVl4A04iB4PS3DaWg/im2C5a83nUTld7Lvy+I6cO1MTaXdkzn6EWXtxkHj7P4VXX sFTr+Z2A6g/novqderirQzq8aD87MBp2hLBgG59lVB3IXA+CJTzBRBZipU+FOSs2 1enMlaEa84d8cb+GSpDkmsvamPMhhLeMw6QLmhQXoWMCAwEAAaNTMFEwHQYDVR0O BBYEFOt9JC+RiqfFdGf/UT2vfmnV8f+kMB8GA1UdIwQYMBaAFOt9JC+RiqfFdGf/ UT2vfmnV8f+kMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBALFX r2CKtu2DcLFQaZft9LeRK67xU0rjN8w1+0MN9otSkU9avd8atPOI9p6r67HZyoTv LDA47ajitzPo4zAXiy4GUXFVCjx3iPOQ4TTCm0YEf9IudLHLqGTbK+Pup6XlfAMb RejkztcXkxw3Cy6SjIRq99xU8J6Y1jAggB162hqnp3u42nA1PgOJgo/biUYeVtu7 Y01gKPdCEkiqmSFxfLiRPv5Z4WyYoKge6UyDYFHu0zyMY7zQ2hzrPMFsEhI4+g5D W7ihilcOijhvfeeWIxcP5lRn2pGbf2GtwqtA7Bt9YKp+NQIBPzK7D1ymS0v/CAWh 3Bv1rqkqDK8TlBybZitTTB6MgGtXOBccouTPmBFBXSWydvW4GW6Dag/ogHHE2vG2 xlXY6EC1QEzExcM5FZNJI6SOaK0nl+WKAv060U/1ZqcRIkhyctYdkrK4449n1JMy wjtwcDW7QxhQspHp8GEXztLctokqGjnuMcgPjVoFdiF3w/IV0UUvVeFK4Oms0YbH uFr3q44Fu/Fol68/1CUk1ytgLUS5anf0Q0WlJsmMUX156ATA29dVBfloJN63EYd7 01uwbjoMJce7MiwTaLIetW75fxxZHlQK9TMNhaQwKUO8SRaNuE4wKURFoKPg/Dqu yPhx9adseStlJ3oV6ziEWMwjOK2JmJf0bmIqQ7KR -----END CERTIFICATE----- ================================================ FILE: modules/elasticsearch/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/elasticsearch/src/test/resources/test-custom-memory-jvm.options ================================================ -Xms1574961152 -Xmx1574961152 ================================================ FILE: modules/gcloud/build.gradle ================================================ description = "Testcontainers :: GCloud" dependencies { api project(':testcontainers') testImplementation platform("com.google.cloud:libraries-bom:26.72.0") testImplementation 'com.google.cloud:google-cloud-bigquery' testImplementation 'com.google.cloud:google-cloud-datastore' testImplementation 'com.google.cloud:google-cloud-firestore' testImplementation 'com.google.cloud:google-cloud-pubsub' testImplementation 'com.google.cloud:google-cloud-spanner' testImplementation 'com.google.cloud:google-cloud-bigtable' } ================================================ FILE: modules/gcloud/src/main/java/org/testcontainers/containers/BigQueryEmulatorContainer.java ================================================ package org.testcontainers.containers; import org.testcontainers.utility.DockerImageName; /** * Testcontainers implementation for BigQuery. *

* Supported image: {@code ghcr.io/goccy/bigquery-emulator} *

* * @deprecated use {@link org.testcontainers.gcloud.BigQueryEmulatorContainer} instead. */ @Deprecated public class BigQueryEmulatorContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("ghcr.io/goccy/bigquery-emulator"); private static final int HTTP_PORT = 9050; private static final int GRPC_PORT = 9060; private static final String PROJECT_ID = "test-project"; public BigQueryEmulatorContainer(String image) { this(DockerImageName.parse(image)); } public BigQueryEmulatorContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); addExposedPorts(HTTP_PORT, GRPC_PORT); withCommand("--project", PROJECT_ID); } public String getEmulatorHttpEndpoint() { return String.format("http://%s:%d", getHost(), getMappedPort(HTTP_PORT)); } public Integer getEmulatorGrpcPort() { return getMappedPort(GRPC_PORT); } public String getProjectId() { return PROJECT_ID; } } ================================================ FILE: modules/gcloud/src/main/java/org/testcontainers/containers/BigtableEmulatorContainer.java ================================================ package org.testcontainers.containers; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; /** * A Bigtable container that relies in google cloud sdk. *

* Supported images: {@code gcr.io/google.com/cloudsdktool/google-cloud-cli}, {@code gcr.io/google.com/cloudsdktool/cloud-sdk} *

* Default port is 9000. * * @deprecated use {@link org.testcontainers.gcloud.BigtableEmulatorContainer} instead. */ @Deprecated public class BigtableEmulatorContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse( "gcr.io/google.com/cloudsdktool/google-cloud-cli" ); private static final DockerImageName CLOUD_SDK_IMAGE_NAME = DockerImageName.parse( "gcr.io/google.com/cloudsdktool/cloud-sdk" ); private static final String CMD = "gcloud beta emulators bigtable start --host-port 0.0.0.0:9000"; private static final int PORT = 9000; public BigtableEmulatorContainer(String image) { this(DockerImageName.parse(image)); } public BigtableEmulatorContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, CLOUD_SDK_IMAGE_NAME); withExposedPorts(PORT); setWaitStrategy(Wait.forLogMessage(".*running.*$", 1)); withCommand("/bin/sh", "-c", CMD); } /** * @return a host:port pair corresponding to the address on which the emulator is * reachable from the test host machine. Directly usable as a parameter to the * com.google.cloud.ServiceOptions.Builder#setHost(java.lang.String) method. */ public String getEmulatorEndpoint() { return getHost() + ":" + getEmulatorPort(); } public int getEmulatorPort() { return getMappedPort(PORT); } } ================================================ FILE: modules/gcloud/src/main/java/org/testcontainers/containers/DatastoreEmulatorContainer.java ================================================ package org.testcontainers.containers; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; /** * A Datastore container that relies in google cloud sdk. *

* Supported images: {@code gcr.io/google.com/cloudsdktool/google-cloud-cli}, {@code gcr.io/google.com/cloudsdktool/cloud-sdk} *

* Default port is 8081. * * @deprecated use {@link org.testcontainers.gcloud.DatastoreEmulatorContainer} instead. */ @Deprecated public class DatastoreEmulatorContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse( "gcr.io/google.com/cloudsdktool/google-cloud-cli" ); private static final DockerImageName CLOUD_SDK_IMAGE_NAME = DockerImageName.parse( "gcr.io/google.com/cloudsdktool/cloud-sdk" ); private static final String PROJECT_ID = "test-project"; private static final String CMD = String.format( "gcloud beta emulators datastore start --project %s --host-port 0.0.0.0:8081", PROJECT_ID ); private static final int HTTP_PORT = 8081; private String flags; public DatastoreEmulatorContainer(final String image) { this(DockerImageName.parse(image)); } public DatastoreEmulatorContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, CLOUD_SDK_IMAGE_NAME); withExposedPorts(HTTP_PORT); setWaitStrategy(Wait.forHttp("/").forStatusCode(200)); } @Override protected void configure() { String command = CMD; if (this.flags != null && !this.flags.isEmpty()) { command += " " + this.flags; } withCommand("/bin/sh", "-c", command); } public DatastoreEmulatorContainer withFlags(String flags) { this.flags = flags; return this; } /** * @return a host:port pair corresponding to the address on which the emulator is * reachable from the test host machine. Directly usable as a parameter to the * com.google.cloud.ServiceOptions.Builder#setHost(java.lang.String) method. */ public String getEmulatorEndpoint() { return getHost() + ":" + getMappedPort(HTTP_PORT); } public String getProjectId() { return PROJECT_ID; } } ================================================ FILE: modules/gcloud/src/main/java/org/testcontainers/containers/FirestoreEmulatorContainer.java ================================================ package org.testcontainers.containers; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; /** * A Firestore container that relies in google cloud sdk. *

* Supported images: {@code gcr.io/google.com/cloudsdktool/google-cloud-cli}, {@code gcr.io/google.com/cloudsdktool/cloud-sdk} *

* Default port is 8080. * * @deprecated use {@link org.testcontainers.gcloud.FirestoreEmulatorContainer} instead. */ @Deprecated public class FirestoreEmulatorContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse( "gcr.io/google.com/cloudsdktool/google-cloud-cli" ); private static final DockerImageName CLOUD_SDK_IMAGE_NAME = DockerImageName.parse( "gcr.io/google.com/cloudsdktool/cloud-sdk" ); private static final String CMD = "gcloud beta emulators firestore start --host-port 0.0.0.0:8080"; private static final int PORT = 8080; private String flags; public FirestoreEmulatorContainer(String image) { this(DockerImageName.parse(image)); } public FirestoreEmulatorContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, CLOUD_SDK_IMAGE_NAME); withExposedPorts(PORT); setWaitStrategy(Wait.forLogMessage(".*running.*$", 1)); } @Override protected void configure() { String command = CMD; if (this.flags != null && !this.flags.isEmpty()) { command += " " + this.flags; } withCommand("/bin/sh", "-c", command); } public FirestoreEmulatorContainer withFlags(String flags) { this.flags = flags; return this; } /** * @return a host:port pair corresponding to the address on which the emulator is * reachable from the test host machine. Directly usable as a parameter to the * com.google.cloud.ServiceOptions.Builder#setHost(java.lang.String) method. */ public String getEmulatorEndpoint() { return getHost() + ":" + getMappedPort(8080); } } ================================================ FILE: modules/gcloud/src/main/java/org/testcontainers/containers/PubSubEmulatorContainer.java ================================================ package org.testcontainers.containers; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; /** * A PubSub container that relies in google cloud sdk. *

* Supported images: {@code gcr.io/google.com/cloudsdktool/google-cloud-cli}, {@code gcr.io/google.com/cloudsdktool/cloud-sdk} *

* Default port is 8085. * * @deprecated use {@link org.testcontainers.gcloud.PubSubEmulatorContainer} instead. */ @Deprecated public class PubSubEmulatorContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse( "gcr.io/google.com/cloudsdktool/google-cloud-cli" ); private static final DockerImageName CLOUD_SDK_IMAGE_NAME = DockerImageName.parse( "gcr.io/google.com/cloudsdktool/cloud-sdk" ); private static final String CMD = "gcloud beta emulators pubsub start --host-port 0.0.0.0:8085"; private static final int PORT = 8085; public PubSubEmulatorContainer(String image) { this(DockerImageName.parse(image)); } public PubSubEmulatorContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, CLOUD_SDK_IMAGE_NAME); withExposedPorts(8085); setWaitStrategy(Wait.forLogMessage(".*started.*$", 1)); withCommand("/bin/sh", "-c", CMD); } /** * @return a host:port pair corresponding to the address on which the emulator is * reachable from the test host machine. Directly usable as a parameter to the * io.grpc.ManagedChannelBuilder#forTarget(java.lang.String) method. */ public String getEmulatorEndpoint() { return getHost() + ":" + getMappedPort(PORT); } } ================================================ FILE: modules/gcloud/src/main/java/org/testcontainers/containers/SpannerEmulatorContainer.java ================================================ package org.testcontainers.containers; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; /** * A Spanner container. Default ports: 9010 for GRPC and 9020 for HTTP. *

* Supported image: {@code gcr.io/cloud-spanner-emulator/emulator} * * @deprecated use {@link org.testcontainers.gcloud.SpannerEmulatorContainer} instead. */ @Deprecated public class SpannerEmulatorContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse( "gcr.io/cloud-spanner-emulator/emulator" ); private static final int GRPC_PORT = 9010; private static final int HTTP_PORT = 9020; public SpannerEmulatorContainer(String image) { this(DockerImageName.parse(image)); } public SpannerEmulatorContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); withExposedPorts(GRPC_PORT, HTTP_PORT); setWaitStrategy(Wait.forLogMessage(".*Cloud Spanner emulator running\\..*", 1)); } /** * @return a host:port pair corresponding to the address on which the emulator's * gRPC endpoint is reachable from the test host machine. Directly usable as a parameter to the * com.google.cloud.spanner.SpannerOptions.Builder#setEmulatorHost(java.lang.String) method. */ public String getEmulatorGrpcEndpoint() { return getHost() + ":" + getMappedPort(GRPC_PORT); } /** * @return a host:port pair corresponding to the address on which the emulator's * HTTP REST endpoint is reachable from the test host machine. */ public String getEmulatorHttpEndpoint() { return getHost() + ":" + getMappedPort(HTTP_PORT); } } ================================================ FILE: modules/gcloud/src/main/java/org/testcontainers/gcloud/BigQueryEmulatorContainer.java ================================================ package org.testcontainers.gcloud; import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.DockerImageName; /** * Testcontainers implementation for BigQuery. *

* Supported image: {@code ghcr.io/goccy/bigquery-emulator} *

*/ public class BigQueryEmulatorContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("ghcr.io/goccy/bigquery-emulator"); private static final int HTTP_PORT = 9050; private static final int GRPC_PORT = 9060; private static final String PROJECT_ID = "test-project"; public BigQueryEmulatorContainer(String image) { this(DockerImageName.parse(image)); } public BigQueryEmulatorContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); addExposedPorts(HTTP_PORT, GRPC_PORT); withCommand("--project", PROJECT_ID); } public String getEmulatorHttpEndpoint() { return String.format("http://%s:%d", getHost(), getMappedPort(HTTP_PORT)); } public Integer getEmulatorGrpcPort() { return getMappedPort(GRPC_PORT); } public String getProjectId() { return PROJECT_ID; } } ================================================ FILE: modules/gcloud/src/main/java/org/testcontainers/gcloud/BigtableEmulatorContainer.java ================================================ package org.testcontainers.gcloud; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; /** * A Bigtable container that relies in google cloud sdk. *

* Supported images: {@code gcr.io/google.com/cloudsdktool/google-cloud-cli}, {@code gcr.io/google.com/cloudsdktool/cloud-sdk} *

* Default port is 9000. */ public class BigtableEmulatorContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse( "gcr.io/google.com/cloudsdktool/google-cloud-cli" ); private static final DockerImageName CLOUD_SDK_IMAGE_NAME = DockerImageName.parse( "gcr.io/google.com/cloudsdktool/cloud-sdk" ); private static final String CMD = "gcloud beta emulators bigtable start --host-port 0.0.0.0:9000"; private static final int PORT = 9000; public BigtableEmulatorContainer(String image) { this(DockerImageName.parse(image)); } public BigtableEmulatorContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, CLOUD_SDK_IMAGE_NAME); withExposedPorts(PORT); setWaitStrategy(Wait.forLogMessage(".*running.*$", 1)); withCommand("/bin/sh", "-c", CMD); } /** * @return a host:port pair corresponding to the address on which the emulator is * reachable from the test host machine. Directly usable as a parameter to the * com.google.cloud.ServiceOptions.Builder#setHost(java.lang.String) method. */ public String getEmulatorEndpoint() { return getHost() + ":" + getEmulatorPort(); } public int getEmulatorPort() { return getMappedPort(PORT); } } ================================================ FILE: modules/gcloud/src/main/java/org/testcontainers/gcloud/DatastoreEmulatorContainer.java ================================================ package org.testcontainers.gcloud; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; /** * A Datastore container that relies in google cloud sdk. *

* Supported images: {@code gcr.io/google.com/cloudsdktool/google-cloud-cli}, {@code gcr.io/google.com/cloudsdktool/cloud-sdk} *

* Default port is 8081. */ public class DatastoreEmulatorContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse( "gcr.io/google.com/cloudsdktool/google-cloud-cli" ); private static final DockerImageName CLOUD_SDK_IMAGE_NAME = DockerImageName.parse( "gcr.io/google.com/cloudsdktool/cloud-sdk" ); private static final String PROJECT_ID = "test-project"; private static final String CMD = String.format( "gcloud beta emulators datastore start --project %s --host-port 0.0.0.0:8081", PROJECT_ID ); private static final int HTTP_PORT = 8081; private String flags; public DatastoreEmulatorContainer(final String image) { this(DockerImageName.parse(image)); } public DatastoreEmulatorContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, CLOUD_SDK_IMAGE_NAME); withExposedPorts(HTTP_PORT); setWaitStrategy(Wait.forHttp("/").forStatusCode(200)); } @Override protected void configure() { String command = CMD; if (this.flags != null && !this.flags.isEmpty()) { command += " " + this.flags; } withCommand("/bin/sh", "-c", command); } public DatastoreEmulatorContainer withFlags(String flags) { this.flags = flags; return this; } /** * @return a host:port pair corresponding to the address on which the emulator is * reachable from the test host machine. Directly usable as a parameter to the * com.google.cloud.ServiceOptions.Builder#setHost(java.lang.String) method. */ public String getEmulatorEndpoint() { return getHost() + ":" + getMappedPort(HTTP_PORT); } public String getProjectId() { return PROJECT_ID; } } ================================================ FILE: modules/gcloud/src/main/java/org/testcontainers/gcloud/FirestoreEmulatorContainer.java ================================================ package org.testcontainers.gcloud; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; /** * A Firestore container that relies in google cloud sdk. *

* Supported images: {@code gcr.io/google.com/cloudsdktool/google-cloud-cli}, {@code gcr.io/google.com/cloudsdktool/cloud-sdk} *

* Default port is 8080. */ public class FirestoreEmulatorContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse( "gcr.io/google.com/cloudsdktool/google-cloud-cli" ); private static final DockerImageName CLOUD_SDK_IMAGE_NAME = DockerImageName.parse( "gcr.io/google.com/cloudsdktool/cloud-sdk" ); private static final String CMD = "gcloud beta emulators firestore start --host-port 0.0.0.0:8080"; private static final int PORT = 8080; private String flags; public FirestoreEmulatorContainer(String image) { this(DockerImageName.parse(image)); } public FirestoreEmulatorContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, CLOUD_SDK_IMAGE_NAME); withExposedPorts(PORT); setWaitStrategy(Wait.forLogMessage(".*running.*$", 1)); } @Override protected void configure() { String command = CMD; if (this.flags != null && !this.flags.isEmpty()) { command += " " + this.flags; } withCommand("/bin/sh", "-c", command); } public FirestoreEmulatorContainer withFlags(String flags) { this.flags = flags; return this; } /** * @return a host:port pair corresponding to the address on which the emulator is * reachable from the test host machine. Directly usable as a parameter to the * com.google.cloud.ServiceOptions.Builder#setHost(java.lang.String) method. */ public String getEmulatorEndpoint() { return getHost() + ":" + getMappedPort(8080); } } ================================================ FILE: modules/gcloud/src/main/java/org/testcontainers/gcloud/PubSubEmulatorContainer.java ================================================ package org.testcontainers.gcloud; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; /** * A PubSub container that relies in google cloud sdk. *

* Supported images: {@code gcr.io/google.com/cloudsdktool/google-cloud-cli}, {@code gcr.io/google.com/cloudsdktool/cloud-sdk} *

* Default port is 8085. */ public class PubSubEmulatorContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse( "gcr.io/google.com/cloudsdktool/google-cloud-cli" ); private static final DockerImageName CLOUD_SDK_IMAGE_NAME = DockerImageName.parse( "gcr.io/google.com/cloudsdktool/cloud-sdk" ); private static final String CMD = "gcloud beta emulators pubsub start --host-port 0.0.0.0:8085"; private static final int PORT = 8085; public PubSubEmulatorContainer(String image) { this(DockerImageName.parse(image)); } public PubSubEmulatorContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, CLOUD_SDK_IMAGE_NAME); withExposedPorts(8085); setWaitStrategy(Wait.forLogMessage(".*started.*$", 1)); withCommand("/bin/sh", "-c", CMD); } /** * @return a host:port pair corresponding to the address on which the emulator is * reachable from the test host machine. Directly usable as a parameter to the * io.grpc.ManagedChannelBuilder#forTarget(java.lang.String) method. */ public String getEmulatorEndpoint() { return getHost() + ":" + getMappedPort(PORT); } } ================================================ FILE: modules/gcloud/src/main/java/org/testcontainers/gcloud/SpannerEmulatorContainer.java ================================================ package org.testcontainers.gcloud; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; /** * A Spanner container. Default ports: 9010 for GRPC and 9020 for HTTP. *

* Supported image: {@code gcr.io/cloud-spanner-emulator/emulator} */ public class SpannerEmulatorContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse( "gcr.io/cloud-spanner-emulator/emulator" ); private static final int GRPC_PORT = 9010; private static final int HTTP_PORT = 9020; public SpannerEmulatorContainer(String image) { this(DockerImageName.parse(image)); } public SpannerEmulatorContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); withExposedPorts(GRPC_PORT, HTTP_PORT); setWaitStrategy(Wait.forLogMessage(".*Cloud Spanner emulator running\\..*", 1)); } /** * @return a host:port pair corresponding to the address on which the emulator's * gRPC endpoint is reachable from the test host machine. Directly usable as a parameter to the * com.google.cloud.spanner.SpannerOptions.Builder#setEmulatorHost(java.lang.String) method. */ public String getEmulatorGrpcEndpoint() { return getHost() + ":" + getMappedPort(GRPC_PORT); } /** * @return a host:port pair corresponding to the address on which the emulator's * HTTP REST endpoint is reachable from the test host machine. */ public String getEmulatorHttpEndpoint() { return getHost() + ":" + getMappedPort(HTTP_PORT); } } ================================================ FILE: modules/gcloud/src/test/java/org/testcontainers/gcloud/BigQueryEmulatorContainerTest.java ================================================ package org.testcontainers.gcloud; import com.google.api.core.ApiFuture; import com.google.api.gax.core.NoCredentialsProvider; import com.google.api.gax.grpc.GrpcTransportChannel; import com.google.api.gax.rpc.FixedTransportChannelProvider; import com.google.cloud.NoCredentials; import com.google.cloud.bigquery.BigQuery; import com.google.cloud.bigquery.BigQueryOptions; import com.google.cloud.bigquery.DatasetId; import com.google.cloud.bigquery.DatasetInfo; import com.google.cloud.bigquery.Field; import com.google.cloud.bigquery.QueryJobConfiguration; import com.google.cloud.bigquery.Schema; import com.google.cloud.bigquery.StandardSQLTypeName; import com.google.cloud.bigquery.StandardTableDefinition; import com.google.cloud.bigquery.TableDefinition; import com.google.cloud.bigquery.TableId; import com.google.cloud.bigquery.TableInfo; import com.google.cloud.bigquery.TableResult; import com.google.cloud.bigquery.storage.v1.AppendRowsResponse; import com.google.cloud.bigquery.storage.v1.BatchCommitWriteStreamsRequest; import com.google.cloud.bigquery.storage.v1.BatchCommitWriteStreamsResponse; import com.google.cloud.bigquery.storage.v1.BigQueryWriteClient; import com.google.cloud.bigquery.storage.v1.BigQueryWriteSettings; import com.google.cloud.bigquery.storage.v1.CreateWriteStreamRequest; import com.google.cloud.bigquery.storage.v1.FinalizeWriteStreamRequest; import com.google.cloud.bigquery.storage.v1.FinalizeWriteStreamResponse; import com.google.cloud.bigquery.storage.v1.JsonStreamWriter; import com.google.cloud.bigquery.storage.v1.TableName; import com.google.cloud.bigquery.storage.v1.WriteStream; import io.grpc.ManagedChannelBuilder; import org.json.JSONArray; import org.json.JSONObject; import org.junit.jupiter.api.Test; import org.threeten.bp.Duration; import java.math.BigDecimal; import java.util.List; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; class BigQueryEmulatorContainerTest { @Test void testHttpEndpoint() throws Exception { try ( // emulatorContainer { BigQueryEmulatorContainer container = new BigQueryEmulatorContainer("ghcr.io/goccy/bigquery-emulator:0.4.3") // } ) { container.start(); // bigQueryClient { String url = container.getEmulatorHttpEndpoint(); BigQueryOptions options = BigQueryOptions .newBuilder() .setProjectId(container.getProjectId()) .setHost(url) .setLocation(url) .setCredentials(NoCredentials.getInstance()) .build(); BigQuery bigQuery = options.getService(); // } String fn = "CREATE FUNCTION testr(arr ARRAY>) AS ((SELECT SUM(IF(elem.name = \"foo\",elem.val,null)) FROM UNNEST(arr) AS elem))"; bigQuery.query(QueryJobConfiguration.newBuilder(fn).build()); String sql = "SELECT testr([STRUCT(\"foo\", 10), STRUCT(\"bar\", 40), STRUCT(\"foo\", 20)])"; TableResult result = bigQuery.query(QueryJobConfiguration.newBuilder(sql).build()); List values = result .streamValues() .map(fieldValues -> fieldValues.get(0).getNumericValue()) .collect(Collectors.toList()); assertThat(values).containsOnly(BigDecimal.valueOf(30)); } } @Test void testGrcpEndpoint() throws Exception { try ( BigQueryEmulatorContainer container = new BigQueryEmulatorContainer("ghcr.io/goccy/bigquery-emulator:0.6.5") ) { container.start(); BigQuery bigQuery = getBigQuery(container); String tableName = "test-table"; String datasetName = "test-dataset"; bigQuery.create(DatasetInfo.of(DatasetId.of(container.getProjectId(), datasetName))); Schema schema = Schema.of(Field.of("name", StandardSQLTypeName.STRING)); TableId tableId = TableId.of(datasetName, tableName); TableDefinition tableDefinition = StandardTableDefinition.of(schema); TableInfo tableInfo = TableInfo.newBuilder(tableId, tableDefinition).build(); bigQuery.create(tableInfo); BigQueryWriteSettings.Builder bigQueryWriteSettingsBuilder = BigQueryWriteSettings.newBuilder(); bigQueryWriteSettingsBuilder .createWriteStreamSettings() .setRetrySettings( bigQueryWriteSettingsBuilder .createWriteStreamSettings() .getRetrySettings() .toBuilder() .setTotalTimeout(Duration.ofSeconds(60)) .build() ); BigQueryWriteClient bigQueryWriteClient = BigQueryWriteClient.create( bigQueryWriteSettingsBuilder .setTransportChannelProvider( FixedTransportChannelProvider.create( GrpcTransportChannel.create( ManagedChannelBuilder .forAddress(container.getHost(), container.getEmulatorGrpcPort()) .usePlaintext() .build() ) ) ) .setCredentialsProvider(NoCredentialsProvider.create()) .build() ); TableName parentTable = TableName.of(container.getProjectId(), datasetName, tableName); CreateWriteStreamRequest createWriteStreamRequest = CreateWriteStreamRequest .newBuilder() .setParent(parentTable.toString()) .setWriteStream(WriteStream.newBuilder().setType(WriteStream.Type.PENDING)) .build(); WriteStream writeStream = bigQueryWriteClient.createWriteStream(createWriteStreamRequest); JsonStreamWriter writer = JsonStreamWriter .newBuilder(writeStream.getName(), writeStream.getTableSchema(), bigQueryWriteClient) .build(); JSONArray jsonArray = new JSONArray(); JSONObject record1 = new JSONObject(); record1.put("name", "Alice"); jsonArray.put(record1); JSONObject record2 = new JSONObject(); record2.put("name", "Bob"); jsonArray.put(record2); ApiFuture future = writer.append(jsonArray); AppendRowsResponse response = future.get(); FinalizeWriteStreamRequest finalizeRequest = FinalizeWriteStreamRequest .newBuilder() .setName(writeStream.getName()) .build(); FinalizeWriteStreamResponse finalizeResponse = bigQueryWriteClient.finalizeWriteStream(finalizeRequest); BatchCommitWriteStreamsRequest commitRequest = BatchCommitWriteStreamsRequest .newBuilder() .setParent(parentTable.toString()) .addWriteStreams(writeStream.getName()) .build(); BatchCommitWriteStreamsResponse commitResponse = bigQueryWriteClient.batchCommitWriteStreams(commitRequest); writer.close(); String sql = String.format( "SELECT name FROM `%s.%s.%s` ORDER BY name", container.getProjectId(), datasetName, tableName ); TableResult result = bigQuery.query(QueryJobConfiguration.newBuilder(sql).build()); List names = result .streamValues() .map(row -> row.get("name").getStringValue()) .collect(Collectors.toList()); assertThat(names).containsExactly("Alice", "Bob"); bigQueryWriteClient.shutdown(); bigQueryWriteClient.close(); } } private BigQuery getBigQuery(BigQueryEmulatorContainer container) { String url = container.getEmulatorHttpEndpoint(); return BigQueryOptions .newBuilder() .setProjectId(container.getProjectId()) .setHost(url) .setLocation(url) .setCredentials(NoCredentials.getInstance()) .build() .getService(); } } ================================================ FILE: modules/gcloud/src/test/java/org/testcontainers/gcloud/BigtableEmulatorContainerTest.java ================================================ package org.testcontainers.gcloud; import com.google.api.gax.core.CredentialsProvider; import com.google.api.gax.core.NoCredentialsProvider; import com.google.api.gax.grpc.GrpcTransportChannel; import com.google.api.gax.rpc.FixedTransportChannelProvider; import com.google.api.gax.rpc.TransportChannelProvider; import com.google.cloud.bigtable.admin.v2.BigtableTableAdminClient; import com.google.cloud.bigtable.admin.v2.models.CreateTableRequest; import com.google.cloud.bigtable.admin.v2.models.Table; import com.google.cloud.bigtable.admin.v2.stub.BigtableTableAdminStubSettings; import com.google.cloud.bigtable.admin.v2.stub.EnhancedBigtableTableAdminStub; import com.google.cloud.bigtable.data.v2.BigtableDataClient; import com.google.cloud.bigtable.data.v2.BigtableDataSettings; import com.google.cloud.bigtable.data.v2.internal.TableAdminRequestContext; import com.google.cloud.bigtable.data.v2.models.Row; import com.google.cloud.bigtable.data.v2.models.RowCell; import com.google.cloud.bigtable.data.v2.models.RowMutation; import com.google.cloud.bigtable.data.v2.models.TableId; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import org.junit.jupiter.api.Test; import org.testcontainers.utility.DockerImageName; import java.io.IOException; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; class BigtableEmulatorContainerTest { private static final String PROJECT_ID = "test-project"; private static final String INSTANCE_ID = "test-instance"; @Test // testWithEmulatorContainer { void testSimple() throws IOException { try ( // emulatorContainer { BigtableEmulatorContainer emulator = new BigtableEmulatorContainer( DockerImageName.parse("gcr.io/google.com/cloudsdktool/google-cloud-cli:441.0.0-emulators") ); // } ) { emulator.start(); ManagedChannel channel = ManagedChannelBuilder .forTarget(emulator.getEmulatorEndpoint()) .usePlaintext() .build(); TransportChannelProvider channelProvider = FixedTransportChannelProvider.create( GrpcTransportChannel.create(channel) ); NoCredentialsProvider credentialsProvider = NoCredentialsProvider.create(); createTable(channelProvider, credentialsProvider, "test-table"); try ( BigtableDataClient client = BigtableDataClient.create( BigtableDataSettings .newBuilderForEmulator(emulator.getHost(), emulator.getEmulatorPort()) .setProjectId(PROJECT_ID) .setInstanceId(INSTANCE_ID) .build() ) ) { client.mutateRow(RowMutation.create(TableId.of("test-table"), "1").setCell("name", "firstName", "Ray")); Row row = client.readRow(TableId.of("test-table"), "1"); List cells = row.getCells("name", "firstName"); assertThat(cells).isNotNull().hasSize(1); assertThat(cells.get(0).getValue().toStringUtf8()).isEqualTo("Ray"); } finally { channel.shutdown(); } } } // } // createTable { private void createTable( TransportChannelProvider channelProvider, CredentialsProvider credentialsProvider, String tableName ) throws IOException { TableAdminRequestContext requestContext = TableAdminRequestContext.create(PROJECT_ID, INSTANCE_ID); EnhancedBigtableTableAdminStub stub = EnhancedBigtableTableAdminStub.createEnhanced( BigtableTableAdminStubSettings .newBuilder() .setTransportChannelProvider(channelProvider) .setCredentialsProvider(credentialsProvider) .build(), requestContext ); try (BigtableTableAdminClient client = BigtableTableAdminClient.create(PROJECT_ID, INSTANCE_ID, stub)) { Table table = client.createTable(CreateTableRequest.of(tableName).addFamily("name")); } } // } } ================================================ FILE: modules/gcloud/src/test/java/org/testcontainers/gcloud/DatastoreEmulatorContainerTest.java ================================================ package org.testcontainers.gcloud; import com.google.cloud.NoCredentials; import com.google.cloud.ServiceOptions; import com.google.cloud.datastore.Datastore; import com.google.cloud.datastore.DatastoreOptions; import com.google.cloud.datastore.Entity; import com.google.cloud.datastore.Key; import org.junit.jupiter.api.Test; import org.testcontainers.utility.DockerImageName; import java.io.IOException; import static org.assertj.core.api.Assertions.assertThat; public class DatastoreEmulatorContainerTest { // startingDatastoreEmulatorContainer { @Test public void testSimple() { try ( // creatingDatastoreEmulatorContainer { DatastoreEmulatorContainer emulator = new DatastoreEmulatorContainer( DockerImageName.parse("gcr.io/google.com/cloudsdktool/google-cloud-cli:441.0.0-emulators") ); // } ) { emulator.start(); DatastoreOptions options = DatastoreOptions .newBuilder() .setHost(emulator.getEmulatorEndpoint()) .setCredentials(NoCredentials.getInstance()) .setRetrySettings(ServiceOptions.getNoRetrySettings()) .setProjectId(emulator.getProjectId()) .build(); Datastore datastore = options.getService(); Key key = datastore.newKeyFactory().setKind("Task").newKey("sample"); Entity entity = Entity.newBuilder(key).set("description", "my description").build(); datastore.put(entity); assertThat(datastore.get(key).getString("description")).isEqualTo("my description"); } } // } @Test void testWithFlags() throws IOException, InterruptedException { try ( DatastoreEmulatorContainer emulator = new DatastoreEmulatorContainer( "gcr.io/google.com/cloudsdktool/google-cloud-cli:441.0.0-emulators" ) .withFlags("--consistency 1.0") ) { emulator.start(); assertThat(emulator.getContainerInfo().getConfig().getCmd()).anyMatch(e -> e.contains("--consistency 1.0")); assertThat(emulator.execInContainer("ls", "/root/.config/").getStdout()).contains("gcloud"); } } @Test void testWithMultipleFlags() throws IOException, InterruptedException { try ( DatastoreEmulatorContainer emulator = new DatastoreEmulatorContainer( "gcr.io/google.com/cloudsdktool/google-cloud-cli:441.0.0-emulators" ) .withFlags("--consistency 1.0 --data-dir /root/.config/test-gcloud") ) { emulator.start(); assertThat(emulator.getContainerInfo().getConfig().getCmd()).anyMatch(e -> e.contains("--consistency 1.0")); assertThat(emulator.execInContainer("ls", "/root/.config/").getStdout()).contains("test-gcloud"); } } } ================================================ FILE: modules/gcloud/src/test/java/org/testcontainers/gcloud/FirestoreEmulatorContainerTest.java ================================================ package org.testcontainers.gcloud; import com.google.api.core.ApiFuture; import com.google.cloud.NoCredentials; import com.google.cloud.firestore.CollectionReference; import com.google.cloud.firestore.DocumentReference; import com.google.cloud.firestore.Firestore; import com.google.cloud.firestore.FirestoreOptions; import com.google.cloud.firestore.QuerySnapshot; import com.google.cloud.firestore.WriteResult; import org.junit.jupiter.api.Test; import org.testcontainers.utility.DockerImageName; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutionException; import static org.assertj.core.api.Assertions.assertThat; class FirestoreEmulatorContainerTest { // testWithEmulatorContainer { @Test void testSimple() throws ExecutionException, InterruptedException { try ( // emulatorContainer { FirestoreEmulatorContainer emulator = new FirestoreEmulatorContainer( DockerImageName.parse("gcr.io/google.com/cloudsdktool/google-cloud-cli:441.0.0-emulators") ); // } ) { emulator.start(); FirestoreOptions options = FirestoreOptions .getDefaultInstance() .toBuilder() .setHost(emulator.getEmulatorEndpoint()) .setCredentials(NoCredentials.getInstance()) .setProjectId("test-project") .build(); Firestore firestore = options.getService(); CollectionReference users = firestore.collection("users"); DocumentReference docRef = users.document("alovelace"); Map data = new HashMap<>(); data.put("first", "Ada"); data.put("last", "Lovelace"); ApiFuture result = docRef.set(data); result.get(); ApiFuture query = users.get(); QuerySnapshot querySnapshot = query.get(); assertThat(querySnapshot.getDocuments().get(0).getData()).containsEntry("first", "Ada"); } } // } @Test void testWithFlags() { try ( FirestoreEmulatorContainer emulator = new FirestoreEmulatorContainer( "gcr.io/google.com/cloudsdktool/google-cloud-cli:465.0.0-emulators" ) .withFlags("--database-mode datastore-mode") ) { emulator.start(); assertThat(emulator.getContainerInfo().getConfig().getCmd()) .anyMatch(e -> e.contains("--database-mode datastore-mode")); } } } ================================================ FILE: modules/gcloud/src/test/java/org/testcontainers/gcloud/PubSubEmulatorContainerTest.java ================================================ package org.testcontainers.gcloud; import com.google.api.gax.core.NoCredentialsProvider; import com.google.api.gax.grpc.GrpcTransportChannel; import com.google.api.gax.rpc.FixedTransportChannelProvider; import com.google.api.gax.rpc.TransportChannelProvider; import com.google.cloud.pubsub.v1.Publisher; import com.google.cloud.pubsub.v1.SubscriptionAdminClient; import com.google.cloud.pubsub.v1.SubscriptionAdminSettings; import com.google.cloud.pubsub.v1.TopicAdminClient; import com.google.cloud.pubsub.v1.TopicAdminSettings; import com.google.cloud.pubsub.v1.stub.GrpcSubscriberStub; import com.google.cloud.pubsub.v1.stub.SubscriberStub; import com.google.cloud.pubsub.v1.stub.SubscriberStubSettings; import com.google.protobuf.ByteString; import com.google.pubsub.v1.ProjectSubscriptionName; import com.google.pubsub.v1.PubsubMessage; import com.google.pubsub.v1.PullRequest; import com.google.pubsub.v1.PullResponse; import com.google.pubsub.v1.PushConfig; import com.google.pubsub.v1.SubscriptionName; import com.google.pubsub.v1.TopicName; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import org.junit.jupiter.api.Test; import org.testcontainers.utility.DockerImageName; import java.io.IOException; import static org.assertj.core.api.Assertions.assertThat; class PubSubEmulatorContainerTest { private static final String PROJECT_ID = "my-project-id"; @Test // testWithEmulatorContainer { void testSimple() throws IOException { try ( // emulatorContainer { PubSubEmulatorContainer emulator = new PubSubEmulatorContainer( DockerImageName.parse("gcr.io/google.com/cloudsdktool/google-cloud-cli:441.0.0-emulators") ); // } ) { emulator.start(); String hostport = emulator.getEmulatorEndpoint(); ManagedChannel channel = ManagedChannelBuilder.forTarget(hostport).usePlaintext().build(); try { TransportChannelProvider channelProvider = FixedTransportChannelProvider.create( GrpcTransportChannel.create(channel) ); NoCredentialsProvider credentialsProvider = NoCredentialsProvider.create(); String topicId = "my-topic-id"; createTopic(topicId, channelProvider, credentialsProvider); String subscriptionId = "my-subscription-id"; createSubscription(subscriptionId, topicId, channelProvider, credentialsProvider); Publisher publisher = Publisher .newBuilder(TopicName.of(PROJECT_ID, topicId)) .setChannelProvider(channelProvider) .setCredentialsProvider(credentialsProvider) .build(); PubsubMessage message = PubsubMessage .newBuilder() .setData(ByteString.copyFromUtf8("test message")) .build(); publisher.publish(message); SubscriberStubSettings subscriberStubSettings = SubscriberStubSettings .newBuilder() .setTransportChannelProvider(channelProvider) .setCredentialsProvider(credentialsProvider) .build(); try (SubscriberStub subscriber = GrpcSubscriberStub.create(subscriberStubSettings)) { PullRequest pullRequest = PullRequest .newBuilder() .setMaxMessages(1) .setSubscription(ProjectSubscriptionName.format(PROJECT_ID, subscriptionId)) .build(); PullResponse pullResponse = subscriber.pullCallable().call(pullRequest); assertThat(pullResponse.getReceivedMessagesList()).hasSize(1); assertThat(pullResponse.getReceivedMessages(0).getMessage().getData().toStringUtf8()) .isEqualTo("test message"); } } finally { channel.shutdown(); } } } // } // createTopic { private void createTopic( String topicId, TransportChannelProvider channelProvider, NoCredentialsProvider credentialsProvider ) throws IOException { TopicAdminSettings topicAdminSettings = TopicAdminSettings .newBuilder() .setTransportChannelProvider(channelProvider) .setCredentialsProvider(credentialsProvider) .build(); try (TopicAdminClient topicAdminClient = TopicAdminClient.create(topicAdminSettings)) { TopicName topicName = TopicName.of(PROJECT_ID, topicId); topicAdminClient.createTopic(topicName); } } // } // createSubscription { private void createSubscription( String subscriptionId, String topicId, TransportChannelProvider channelProvider, NoCredentialsProvider credentialsProvider ) throws IOException { SubscriptionAdminSettings subscriptionAdminSettings = SubscriptionAdminSettings .newBuilder() .setTransportChannelProvider(channelProvider) .setCredentialsProvider(credentialsProvider) .build(); SubscriptionAdminClient subscriptionAdminClient = SubscriptionAdminClient.create(subscriptionAdminSettings); SubscriptionName subscriptionName = SubscriptionName.of(PROJECT_ID, subscriptionId); subscriptionAdminClient.createSubscription( subscriptionName, TopicName.of(PROJECT_ID, topicId), PushConfig.getDefaultInstance(), 10 ); } // } } ================================================ FILE: modules/gcloud/src/test/java/org/testcontainers/gcloud/SpannerEmulatorContainerTest.java ================================================ package org.testcontainers.gcloud; import com.google.cloud.NoCredentials; import com.google.cloud.spanner.Database; import com.google.cloud.spanner.DatabaseAdminClient; import com.google.cloud.spanner.DatabaseClient; import com.google.cloud.spanner.DatabaseId; import com.google.cloud.spanner.Instance; import com.google.cloud.spanner.InstanceAdminClient; import com.google.cloud.spanner.InstanceConfigId; import com.google.cloud.spanner.InstanceId; import com.google.cloud.spanner.InstanceInfo; import com.google.cloud.spanner.ResultSet; import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerOptions; import com.google.cloud.spanner.Statement; import org.junit.jupiter.api.Test; import org.testcontainers.utility.DockerImageName; import java.util.Arrays; import java.util.concurrent.ExecutionException; import static org.assertj.core.api.Assertions.assertThat; class SpannerEmulatorContainerTest { private static final String PROJECT_NAME = "test-project"; private static final String INSTANCE_NAME = "test-instance"; private static final String DATABASE_NAME = "test-database"; // testWithEmulatorContainer { @Test void testSimple() throws ExecutionException, InterruptedException { try ( // emulatorContainer { SpannerEmulatorContainer emulator = new SpannerEmulatorContainer( DockerImageName.parse("gcr.io/cloud-spanner-emulator/emulator:1.4.0") ); // } ) { emulator.start(); SpannerOptions options = SpannerOptions .newBuilder() .setEmulatorHost(emulator.getEmulatorGrpcEndpoint()) .setCredentials(NoCredentials.getInstance()) .setProjectId(PROJECT_NAME) .build(); Spanner spanner = options.getService(); InstanceId instanceId = createInstance(spanner); createDatabase(spanner); DatabaseId databaseId = DatabaseId.of(instanceId, DATABASE_NAME); DatabaseClient dbClient = spanner.getDatabaseClient(databaseId); dbClient .readWriteTransaction() .run(tx -> { String sql1 = "Delete from TestTable where 1=1"; tx.executeUpdate(Statement.of(sql1)); String sql = "INSERT INTO TestTable (Key, Value) VALUES (1, 'Java'), (2, 'Go')"; tx.executeUpdate(Statement.of(sql)); return null; }); ResultSet resultSet = dbClient .readOnlyTransaction() .executeQuery(Statement.of("select * from TestTable order by Key")); resultSet.next(); assertThat(resultSet.getLong(0)).isEqualTo(1); assertThat(resultSet.getString(1)).isEqualTo("Java"); } } // } // createDatabase { private void createDatabase(Spanner spanner) throws InterruptedException, ExecutionException { DatabaseAdminClient dbAdminClient = spanner.getDatabaseAdminClient(); Database database = dbAdminClient .createDatabase( INSTANCE_NAME, DATABASE_NAME, Arrays.asList("CREATE TABLE TestTable (Key INT64, Value STRING(MAX)) PRIMARY KEY (Key)") ) .get(); } // } // createInstance { private InstanceId createInstance(Spanner spanner) throws InterruptedException, ExecutionException { InstanceConfigId instanceConfig = InstanceConfigId.of(PROJECT_NAME, "emulator-config"); InstanceId instanceId = InstanceId.of(PROJECT_NAME, INSTANCE_NAME); InstanceAdminClient insAdminClient = spanner.getInstanceAdminClient(); Instance instance = insAdminClient .createInstance( InstanceInfo .newBuilder(instanceId) .setNodeCount(1) .setDisplayName("Test instance") .setInstanceConfigId(instanceConfig) .build() ) .get(); return instanceId; } // } } ================================================ FILE: modules/gcloud/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/grafana/build.gradle ================================================ description = "Testcontainers :: Grafana" dependencies { api project(':testcontainers') testImplementation 'io.rest-assured:rest-assured:5.5.6' testImplementation 'io.micrometer:micrometer-registry-otlp:1.16.1' testImplementation 'uk.org.webcompere:system-stubs-jupiter:2.1.8' testImplementation platform('io.opentelemetry:opentelemetry-bom:1.57.0') testImplementation 'io.opentelemetry:opentelemetry-api' testImplementation 'io.opentelemetry:opentelemetry-sdk' testImplementation 'io.opentelemetry:opentelemetry-exporter-otlp' } ================================================ FILE: modules/grafana/src/main/java/org/testcontainers/grafana/LgtmStackContainer.java ================================================ package org.testcontainers.grafana; import com.github.dockerjava.api.command.InspectContainerResponse; import lombok.extern.slf4j.Slf4j; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; /** * Testcontainers implementation for Grafana OTel LGTM. *

* Supported image: {@code grafana/otel-lgtm} *

* Exposed ports: *

    *
  • Grafana: 3000
  • *
  • Tempo: 3200
  • *
  • OTel Http: 4317
  • *
  • OTel Grpc: 4318
  • *
  • Prometheus: 9090
  • *
*/ @Slf4j public class LgtmStackContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("grafana/otel-lgtm"); private static final int GRAFANA_PORT = 3000; private static final int OTLP_GRPC_PORT = 4317; private static final int OTLP_HTTP_PORT = 4318; private static final int LOKI_PORT = 3100; private static final int TEMPO_PORT = 3200; private static final int PROMETHEUS_PORT = 9090; public LgtmStackContainer(String image) { this(DockerImageName.parse(image)); } public LgtmStackContainer(DockerImageName image) { super(image); image.assertCompatibleWith(DEFAULT_IMAGE_NAME); withExposedPorts(GRAFANA_PORT, TEMPO_PORT, LOKI_PORT, OTLP_GRPC_PORT, OTLP_HTTP_PORT, PROMETHEUS_PORT); waitingFor( Wait.forLogMessage(".*The OpenTelemetry collector and the Grafana LGTM stack are up and running.*\\s", 1) ); } @Override protected void containerIsStarted(InspectContainerResponse containerInfo) { log.info("Access to the Grafana dashboard: {}", getGrafanaHttpUrl()); } public String getOtlpGrpcUrl() { return "http://" + getHost() + ":" + getMappedPort(OTLP_GRPC_PORT); } public String getTempoUrl() { return "http://" + getHost() + ":" + getMappedPort(TEMPO_PORT); } public String getLokiUrl() { return "http://" + getHost() + ":" + getMappedPort(LOKI_PORT); } public String getOtlpHttpUrl() { return "http://" + getHost() + ":" + getMappedPort(OTLP_HTTP_PORT); } public String getPrometheusHttpUrl() { return "http://" + getHost() + ":" + getMappedPort(PROMETHEUS_PORT); } public String getGrafanaHttpUrl() { return "http://" + getHost() + ":" + getMappedPort(GRAFANA_PORT); } } ================================================ FILE: modules/grafana/src/test/java/org/testcontainers/grafana/LgtmStackContainerTest.java ================================================ package org.testcontainers.grafana; import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.registry.otlp.OtlpConfig; import io.micrometer.registry.otlp.OtlpMeterRegistry; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.logs.Logger; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter; import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.logs.SdkLoggerProvider; import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; import io.restassured.RestAssured; import io.restassured.response.Response; import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; import uk.org.webcompere.systemstubs.SystemStubs; import java.time.Duration; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; class LgtmStackContainerTest { @Test void shouldPublishMetricsTracesAndLogs() throws Exception { try ( // container { LgtmStackContainer lgtm = new LgtmStackContainer("grafana/otel-lgtm:0.11.1") // } ) { lgtm.start(); OtlpGrpcSpanExporter spanExporter = OtlpGrpcSpanExporter .builder() .setTimeout(Duration.ofSeconds(1)) .setEndpoint(lgtm.getOtlpGrpcUrl()) .build(); OtlpGrpcLogRecordExporter logExporter = OtlpGrpcLogRecordExporter .builder() .setTimeout(Duration.ofSeconds(1)) .setEndpoint(lgtm.getOtlpGrpcUrl()) .build(); BatchSpanProcessor spanProcessor = BatchSpanProcessor .builder(spanExporter) .setScheduleDelay(500, TimeUnit.MILLISECONDS) .build(); SdkTracerProvider tracerProvider = SdkTracerProvider .builder() .addSpanProcessor(spanProcessor) .setResource(Resource.create(Attributes.of(AttributeKey.stringKey("service.name"), "test-service"))) .build(); SdkLoggerProvider loggerProvider = SdkLoggerProvider .builder() .addLogRecordProcessor(SimpleLogRecordProcessor.create(logExporter)) .build(); OpenTelemetrySdk openTelemetry = OpenTelemetrySdk .builder() .setTracerProvider(tracerProvider) .setLoggerProvider(loggerProvider) .build(); String version = RestAssured .get(String.format("http://%s:%s/api/health", lgtm.getHost(), lgtm.getMappedPort(3000))) .jsonPath() .get("version"); assertThat(version).isEqualTo("12.0.0"); OtlpConfig otlpConfig = createOtlpConfig(lgtm); MeterRegistry meterRegistry = SystemStubs .withEnvironmentVariable("OTEL_SERVICE_NAME", "testcontainers") .execute(() -> new OtlpMeterRegistry(otlpConfig, Clock.SYSTEM)); Counter.builder("test.counter").register(meterRegistry).increment(2); Logger logger = openTelemetry.getSdkLoggerProvider().loggerBuilder("test").build(); logger .logRecordBuilder() .setBody("Test log!") .setAttribute(AttributeKey.stringKey("job"), "test-job") .emit(); Tracer tracer = openTelemetry.getTracer("test"); Span span = tracer.spanBuilder("test").startSpan(); span.end(); Awaitility .given() .pollInterval(Duration.ofSeconds(2)) .atMost(Duration.ofSeconds(5)) .ignoreExceptions() .untilAsserted(() -> { Response metricResponse = RestAssured .given() .queryParam("query", "test_counter_total{job=\"testcontainers\"}") .get(String.format("%s/api/v1/query", lgtm.getPrometheusHttpUrl())) .prettyPeek() .thenReturn(); assertThat(metricResponse.getStatusCode()).isEqualTo(200); assertThat(metricResponse.body().jsonPath().getList("data.result[0].value")).contains("2"); Response logResponse = RestAssured .given() .queryParam("query", "{service_name=\"unknown_service:java\"}") .get(String.format("%s/loki/api/v1/query_range", lgtm.getLokiUrl())) .prettyPeek() .thenReturn(); assertThat(logResponse.getStatusCode()).isEqualTo(200); assertThat(logResponse.body().jsonPath().getString("data.result[0].values[0][1]")) .isEqualTo("Test log!"); Response traceResponse = RestAssured .given() .get(String.format("%s/api/search", lgtm.getTempoUrl())) .prettyPeek() .thenReturn(); assertThat(traceResponse.getStatusCode()).isEqualTo(200); assertThat(traceResponse.body().jsonPath().getString("traces[0].rootServiceName")) .isEqualTo("test-service"); }); openTelemetry.close(); } } private static OtlpConfig createOtlpConfig(LgtmStackContainer lgtm) { return new OtlpConfig() { @Override public String url() { return String.format("%s/v1/metrics", lgtm.getOtlpHttpUrl()); } @Override public Duration step() { return Duration.ofSeconds(1); } @Override public String get(String s) { return null; } }; } } ================================================ FILE: modules/grafana/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/hivemq/build.gradle ================================================ description = "Testcontainers :: HiveMQ" dependencies { api(project(":testcontainers")) api("org.jetbrains:annotations:26.0.2-1") shaded("org.apache.commons:commons-lang3:3.20.0") shaded("commons-io:commons-io:2.21.0") shaded("org.javassist:javassist:3.30.2-GA") shaded("org.jboss.shrinkwrap:shrinkwrap-api:1.2.6") shaded("org.jboss.shrinkwrap:shrinkwrap-impl-base:1.2.6") shaded("net.lingala.zip4j:zip4j:2.11.5") testImplementation(project(":testcontainers-junit-jupiter")) testImplementation("com.hivemq:hivemq-extension-sdk:4.47.1") testImplementation("com.hivemq:hivemq-mqtt-client:1.3.10") testImplementation("org.apache.httpcomponents:httpclient:4.5.14") testImplementation("ch.qos.logback:logback-classic:1.5.22") } test { javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(11) } } compileTestJava { javaCompiler = javaToolchains.compilerFor { languageVersion = JavaLanguageVersion.of(11) } options.release.set(11) } ================================================ FILE: modules/hivemq/src/main/java/org/testcontainers/hivemq/HiveMQContainer.java ================================================ package org.testcontainers.hivemq; import com.github.dockerjava.api.command.InspectContainerResponse; import org.apache.commons.io.FileUtils; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.event.Level; import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.containers.wait.strategy.WaitAllStrategy; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; /** * Testcontainers implementation for HiveMQ. *

* Supported images: {@code hivemq/hivemq4}, {@code hivemq/hivemq-ce} *

* Exposed ports: *

    *
  • MQTT: 1883
  • *
  • Control Center: 8080
  • *
  • Debug: 9000
  • *
*/ public class HiveMQContainer extends GenericContainer { private static final Logger LOGGER = LoggerFactory.getLogger(HiveMQContainer.class); private static final DockerImageName DEFAULT_HIVEMQ_EE_IMAGE_NAME = DockerImageName.parse("hivemq/hivemq4"); private static final DockerImageName DEFAULT_HIVEMQ_CE_IMAGE_NAME = DockerImageName.parse("hivemq/hivemq-ce"); private static final int DEBUGGING_PORT = 9000; private static final int MQTT_PORT = 1883; private static final int CONTROL_CENTER_PORT = 8080; @SuppressWarnings("OctalInteger") private static final int MODE = 0777; @NotNull private static final Pattern EXTENSION_ID_PATTERN = Pattern.compile("(.+?)"); @NotNull private final ConcurrentHashMap containerOutputLatches = new ConcurrentHashMap<>(); private boolean controlCenterEnabled = false; private boolean debugging = false; @NotNull private final Set prepackagedExtensionsToRemove = new HashSet<>(); private boolean removeAllPrepackagedExtensions = false; @NotNull private final WaitAllStrategy waitStrategy = new WaitAllStrategy(); public HiveMQContainer(final @NotNull DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_HIVEMQ_CE_IMAGE_NAME, DEFAULT_HIVEMQ_EE_IMAGE_NAME); addExposedPort(MQTT_PORT); waitStrategy.withStrategy(Wait.forLogMessage("(.*)Started HiveMQ in(.*)", 1)); waitingFor(waitStrategy); withLogConsumer(outputFrame -> { final String utf8String = outputFrame.getUtf8String(); if (debugging && utf8String.startsWith("Listening for transport dt_socket at address:")) { System.out.println("Listening for transport dt_socket at address: " + getMappedPort(DEBUGGING_PORT)); } if (!containerOutputLatches.isEmpty()) { containerOutputLatches.forEach((regEx, latch) -> { if (outputFrame.getUtf8String().matches("(?s)" + regEx)) { LOGGER.debug("Container Output '{}' matched RegEx '{}'", utf8String, regEx); latch.countDown(); } else { LOGGER.debug("Container Output '{}' did not match RegEx '{}'", utf8String, regEx); } }); } }); final HashMap tmpFs = new HashMap<>(); if (dockerImageName.isCompatibleWith(DEFAULT_HIVEMQ_EE_IMAGE_NAME)) { tmpFs.put("/opt/hivemq/audit", "rw"); tmpFs.put("/opt/hivemq/backup", "rw"); } tmpFs.put("/opt/hivemq/log", "rw"); tmpFs.put("/opt/hivemq/data", "rw"); withTmpFs(tmpFs); } @Override protected void configure() { final String removeCommand; withCreateContainerCmdModifier(it -> it.withEntrypoint("/bin/sh")); if (removeAllPrepackagedExtensions || !prepackagedExtensionsToRemove.isEmpty()) { if (removeAllPrepackagedExtensions) { removeCommand = "rm -rf /opt/hivemq/extensions/** &&"; } else { removeCommand = prepackagedExtensionsToRemove .stream() .map(extensionId -> "rm -rf /opt/hivemq/extensions/" + extensionId + "&&") .collect(Collectors.joining()); } } else { removeCommand = ""; } setCommand( "-c", removeCommand + "cp -r '/opt/hivemq/temp-extensions/'* /opt/hivemq/extensions/ ; " + "chmod -R 777 /opt/hivemq/extensions ; " + "/opt/docker-entrypoint.sh /opt/hivemq/bin/run.sh" ); } protected void containerIsStarted(final @NotNull InspectContainerResponse containerInfo) { if (controlCenterEnabled) { LOGGER.info( "The HiveMQ Control Center is reachable under: http://{}:{}", getHost(), getMappedPort(CONTROL_CENTER_PORT) ); } } /** * Adds a wait condition for the extension with this name. *

* Must be called before the container is started. * * @param extensionName the extension to wait for * @return self */ public @NotNull HiveMQContainer waitForExtension(final @NotNull String extensionName) { final String regEX = "(.*)Extension \"" + extensionName + "\" version (.*) started successfully(.*)"; waitStrategy.withStrategy(Wait.forLogMessage(regEX, 1)); return self(); } /** * Adds a wait condition for this {@link HiveMQExtension} *

* Must be called before the container is started. * * @param extension the extension to wait for * @return self */ public @NotNull HiveMQContainer waitForExtension(final @NotNull HiveMQExtension extension) { return this.waitForExtension(extension.getName()); } /** * Enables the possibility for remote debugging clients to connect. *

* Must be called before the container is started. * * @return self */ public @NotNull HiveMQContainer withDebugging() { debugging = true; addExposedPorts(DEBUGGING_PORT); withEnv( "JAVA_OPTS", "-agentlib:jdwp=transport=dt_socket,address=0.0.0.0:" + DEBUGGING_PORT + ",server=y,suspend=y" ); return self(); } /** * Sets the logging {@link Level} inside the container. *

* Must be called before the container is started. * * @param level the {@link Level} * @return self */ public @NotNull HiveMQContainer withLogLevel(final @NotNull Level level) { this.withEnv("HIVEMQ_LOG_LEVEL", level.name()); return self(); } /** * Wraps the given class and all its subclasses into an extension * and puts it into '/opt/hivemq/temp-extensions/{extension-id}' inside the container. *

* Must be called before the container is started. *

* The contents of the '/opt/hivemq/temp-extensions/' directory are copied to '/opt/hivemq/extensions/' before the container is started. * * @param hiveMQExtension the {@link HiveMQExtension} of the extension * @return self */ public @NotNull HiveMQContainer withExtension(final @NotNull HiveMQExtension hiveMQExtension) { try { final File extension = hiveMQExtension.createExtension(hiveMQExtension); final MountableFile mountableExtension = MountableFile.forHostPath(extension.getPath(), MODE); withCopyFileToContainer(mountableExtension, "/opt/hivemq/temp-extensions/" + hiveMQExtension.getId()); } catch (final Exception e) { throw new ContainerLaunchException(e.getMessage() == null ? "" : e.getMessage(), e); } return self(); } /** * Puts the given extension folder into '/opt/hivemq/temp-extensions/{directory-name}' inside the container. * It must at least contain a valid hivemq-extension.xml and a valid extension.jar in order to be executed. * The directory-name is taken from the id defined in the hivemq-extension.xml. *

* Must be called before the container is started. *

* The contents of the '/opt/hivemq/temp-extensions/' directory are copied to '/opt/hivemq/extensions/' before the container is started. * * @param mountableExtension the extension folder on the host machine * @return self */ public @NotNull HiveMQContainer withExtension(final @NotNull MountableFile mountableExtension) { final File extensionDir = new File(mountableExtension.getResolvedPath()); if (!extensionDir.exists()) { throw new ContainerLaunchException( "Extension '" + mountableExtension.getFilesystemPath() + "' could not be mounted. It does not exist." ); } if (!extensionDir.isDirectory()) { throw new ContainerLaunchException( "Extension '" + mountableExtension.getFilesystemPath() + "' could not be mounted. It is not a directory." ); } try { final String extensionDirName = getExtensionDirectoryName(extensionDir); final String containerPath = "/opt/hivemq/temp-extensions/" + extensionDirName; withCopyFileToContainer(cloneWithFileMode(mountableExtension), containerPath); LOGGER.info("Putting extension '{}' into '{}'", extensionDirName, containerPath); } catch (final Exception e) { throw new ContainerLaunchException(e.getMessage() == null ? "" : e.getMessage(), e); } return self(); } private @NotNull String getExtensionDirectoryName(final @NotNull File extensionDirectory) throws IOException { final File file = new File(extensionDirectory, "hivemq-extension.xml"); final String xml = FileUtils.readFileToString(file, StandardCharsets.UTF_8); final Matcher matcher = EXTENSION_ID_PATTERN.matcher(xml); if (!matcher.find()) { throw new IllegalStateException("Could not parse extension id from '" + file.getAbsolutePath() + "'"); } return matcher.group(1); } /** * Removes the specified prepackaged extension folders from '/opt/hivemq/extensions' before the container is started. *

* Must be called before the container is started. * * @param extensionIds the prepackaged extensions to remove * @return self */ public @NotNull HiveMQContainer withoutPrepackagedExtensions(final @NotNull String... extensionIds) { Collections.addAll(prepackagedExtensionsToRemove, extensionIds); return self(); } /** * Removes all prepackaged extension folders from '/opt/hivemq/extensions' before the container is started. *

* Must be called before the container is started. * * @return self */ public @NotNull HiveMQContainer withoutPrepackagedExtensions() { removeAllPrepackagedExtensions = true; return self(); } /** * Puts the given license into '/opt/hivemq/license/' inside the container. * It must end with '.lic' or '.elic'. *

* Must be called before the container is started. * * @param mountableLicense the license file on the host machine * @return self */ public @NotNull HiveMQContainer withLicense(final @NotNull MountableFile mountableLicense) { final File licenseFile = new File(mountableLicense.getResolvedPath()); if (!licenseFile.exists()) { throw new ContainerLaunchException( "License file '" + mountableLicense.getFilesystemPath() + "' does not exist." ); } if (!licenseFile.getName().endsWith(".lic") && !licenseFile.getName().endsWith(".elic")) { throw new ContainerLaunchException( "License file '" + mountableLicense.getFilesystemPath() + "' does not end wit '.lic' or '.elic'." ); } final String containerPath = "/opt/hivemq/license/" + licenseFile.getName(); withCopyFileToContainer(cloneWithFileMode(mountableLicense), containerPath); LOGGER.info("Putting license '{}' into '{}'.", licenseFile.getAbsolutePath(), containerPath); return self(); } /** * Overwrites the HiveMQ configuration in '/opt/hivemq/conf/' inside the container. *

* Must be called before the container is started. * * @param mountableConfig the config file on the host machine * @return self */ public @NotNull HiveMQContainer withHiveMQConfig(final @NotNull MountableFile mountableConfig) { final File config = new File(mountableConfig.getResolvedPath()); if (!config.exists()) { throw new ContainerLaunchException( "HiveMQ config file '" + mountableConfig.getFilesystemPath() + "' does not exist." ); } final String containerPath = "/opt/hivemq/conf/config.xml"; withCopyFileToContainer(cloneWithFileMode(mountableConfig), containerPath); LOGGER.info("Putting '{}' into '{}'.", config.getAbsolutePath(), containerPath); return self(); } /** * Puts the given file into the root of the extension's home '/opt/hivemq/temp-extensions/{extensionId}/'. *

* Must be called before the container is started. *

* The contents of the '/opt/hivemq/temp-extensions/' directory are copied to '/opt/hivemq/extensions/' before the container is started. * * @param file the file on the host machine * @param extensionId the extension * @return self */ public @NotNull HiveMQContainer withFileInExtensionHomeFolder( final @NotNull MountableFile file, final @NotNull String extensionId ) { return withFileInExtensionHomeFolder(file, extensionId, ""); } /** * Puts the given file into given subdirectory of the extensions's home '/opt/hivemq/temp-extensions/{id}/{pathInExtensionHome}/' *

* Must be called before the container is started. *

* The contents of the '/opt/hivemq/temp-extensions/' directory are copied to '/opt/hivemq/extensions/' before the container is started. * * @param file the file on the host machine * @param extensionId the extension * @param pathInExtensionHome the path * @return self */ public @NotNull HiveMQContainer withFileInExtensionHomeFolder( final @NotNull MountableFile file, final @NotNull String extensionId, final @NotNull String pathInExtensionHome ) { return withFileInHomeFolder( file, "/temp-extensions/" + extensionId + PathUtil.prepareAppendPath(pathInExtensionHome) ); } /** * Puts the given file into the given subdirectory of the HiveMQ home folder '/opt/hivemq/{pathInHomeFolder}'. *

* Must be called before the container is started. * * @param mountableFile the file on the host machine * @param pathInHomeFolder the path * @return self */ public @NotNull HiveMQContainer withFileInHomeFolder( final @NotNull MountableFile mountableFile, final @NotNull String pathInHomeFolder ) { final File file = new File(mountableFile.getResolvedPath()); if (pathInHomeFolder.trim().isEmpty()) { throw new ContainerLaunchException("pathInHomeFolder must not be empty"); } if (!file.exists()) { throw new ContainerLaunchException("File '" + mountableFile.getFilesystemPath() + "' does not exist."); } final String containerPath = "/opt/hivemq" + PathUtil.prepareAppendPath(pathInHomeFolder); withCopyFileToContainer(cloneWithFileMode(mountableFile), containerPath); LOGGER.info("Putting file '{}' into container path '{}'.", file.getAbsolutePath(), containerPath); return self(); } /** * Disables the extension with the given name and extension directory name. * This method blocks until the HiveMQ log for successful disabling is consumed or it times out after {timeOut}. * Note: Disabling Extensions is a HiveMQ Enterprise feature, it will not work when using the HiveMQ Community Edition. *

* This can only be called once the container is started. * * @param extensionName the name of the extension to disable * @param extensionDirectory the name of the extension's directory * @param timeout the timeout * @throws TimeoutException if the extension was not disabled within the configured timeout */ public void disableExtension( final @NotNull String extensionName, final @NotNull String extensionDirectory, final @NotNull Duration timeout ) throws TimeoutException { final String regEX = "(.*)Extension \"" + extensionName + "\" version (.*) stopped successfully(.*)"; try { final String containerPath = "/opt/hivemq/extensions" + PathUtil.prepareInnerPath(extensionDirectory) + "DISABLED"; final CountDownLatch latch = new CountDownLatch(1); containerOutputLatches.put(regEX, latch); execInContainer("touch", containerPath); LOGGER.info("Putting DISABLED file into container path '{}'", containerPath); final boolean await = latch.await(timeout.getSeconds(), TimeUnit.SECONDS); if (!await) { throw new TimeoutException( "Extension disabling timed out after '" + timeout.getSeconds() + "' seconds. " + "Maybe you are using a HiveMQ Community Edition image, " + "which does not support disabling of extensions" ); } } catch (final InterruptedException | IOException e) { throw new RuntimeException(e); } finally { containerOutputLatches.remove(regEX); } } /** * Disables the extension with the given name and extension directory name. * This method blocks until the HiveMQ log for successful disabling is consumed or it times out after 60 seconds. * Note: Disabling Extensions is a HiveMQ Enterprise feature, it will not work when using the HiveMQ Community Edition. *

* This can only be called once the container is started. * * @param extensionName the name of the extension to disable * @param extensionDirectory the name of the extension's directory * @throws TimeoutException if the extension was not disabled within 60 seconds */ public void disableExtension(final @NotNull String extensionName, final @NotNull String extensionDirectory) throws TimeoutException { disableExtension(extensionName, extensionDirectory, Duration.ofSeconds(60)); } /** * Disables the extension. * This method blocks until the HiveMQ log for successful disabling is consumed or it times out after {timeOut}. * Note: Disabling Extensions is a HiveMQ Enterprise feature, it will not work when using the HiveMQ Community Edition. *

* This can only be called once the container is started. * * @param hiveMQExtension the extension * @param timeout the timeout * @throws TimeoutException if the extension was not disabled within the configured timeout */ public void disableExtension(final @NotNull HiveMQExtension hiveMQExtension, final @NotNull Duration timeout) throws TimeoutException { disableExtension(hiveMQExtension.getName(), hiveMQExtension.getId(), timeout); } /** * Disables the extension. * This method blocks until the HiveMQ log for successful disabling is consumed or it times out after 60 seconds. * Note: Disabling Extensions is a HiveMQ Enterprise feature, it will not work when using the HiveMQ Community Edition. *

* This can only be called once the container is started. * * @param hiveMQExtension the extension * @throws TimeoutException if the extension was not disabled within 60 seconds */ public void disableExtension(final @NotNull HiveMQExtension hiveMQExtension) throws TimeoutException { disableExtension(hiveMQExtension, Duration.ofSeconds(60)); } /** * Enables the extension with the given name and extension directory name. * This method blocks until the HiveMQ log for successful enabling is consumed or it times out after {timeOut}. * Note: Enabling Extensions is a HiveMQ Enterprise feature, it will not work when using the HiveMQ Community Edition. *

* This can only be called once the container is started. * * @param extensionName the name of the extension to disable * @param extensionDirectory the name of the extension's directory * @param timeout the timeout * @throws TimeoutException if the extension was not enabled within the configured timeout */ public void enableExtension( final @NotNull String extensionName, final @NotNull String extensionDirectory, final @NotNull Duration timeout ) throws TimeoutException { final String regEX = "(.*)Extension \"" + extensionName + "\" version (.*) started successfully(.*)"; try { final String containerPath = "/opt/hivemq/extensions" + PathUtil.prepareInnerPath(extensionDirectory) + "DISABLED"; final CountDownLatch latch = new CountDownLatch(1); containerOutputLatches.put(regEX, latch); execInContainer("rm", "-rf", containerPath); LOGGER.info("Removing DISABLED file in container path '{}'", containerPath); final boolean await = latch.await(timeout.getSeconds(), TimeUnit.SECONDS); if (!await) { throw new TimeoutException( "Extension enabling timed out after '" + timeout.getSeconds() + "' seconds. " + "Maybe you are using a HiveMQ Community Edition image, " + "which does not support disabling of extensions" ); } } catch (final InterruptedException | IOException e) { throw new RuntimeException(e); } finally { containerOutputLatches.remove(regEX); } } /** * Enables the extension with the given name and extension directory name. * This method blocks until the HiveMQ log for successful enabling is consumed or it times out after 60 seconds. * Note: Enabling Extensions is a HiveMQ Enterprise feature, it will not work when using the HiveMQ Community Edition. *

* This can only be called once the container is started. * * @param extensionName the name of the extension to disable * @param extensionDirectory the name of the extension's directory * @throws TimeoutException if the extension was not enabled within 60 seconds */ public void enableExtension(final @NotNull String extensionName, final @NotNull String extensionDirectory) throws TimeoutException { enableExtension(extensionName, extensionDirectory, Duration.ofSeconds(60)); } /** * Enables the extension. * This method blocks until the HiveMQ log for successful enabling is consumed or it times out after {timeOut}. * Note: Enabling Extensions is a HiveMQ Enterprise feature, it will not work when using the HiveMQ Community Edition. *

* This can only be called once the container is started. * * @param hiveMQExtension the extension * @param timeout the timeout * @throws TimeoutException if the extension was not enabled within the configured timeout */ public void enableExtension(final @NotNull HiveMQExtension hiveMQExtension, final @NotNull Duration timeout) throws TimeoutException { enableExtension(hiveMQExtension.getName(), hiveMQExtension.getId(), timeout); } /** * Enables the extension. * This method blocks until the HiveMQ log for successful enabling is consumed or it times out after {timeOut}. * Note: Enabling Extensions is a HiveMQ Enterprise feature, it will not work when using the HiveMQ Community Edition. *

* This can only be called once the container is started. * * @param hiveMQExtension the extension * @throws TimeoutException if the extension was not enabled within 60 seconds */ public void enableExtension(final @NotNull HiveMQExtension hiveMQExtension) throws TimeoutException { enableExtension(hiveMQExtension, Duration.ofSeconds(60)); } /** * Enables connection to the HiveMQ Control Center on host port 8080. * Note: the control center is a HiveMQ 4 Enterprise feature. *

* Must be called before the container is started. * * @return self */ public @NotNull HiveMQContainer withControlCenter() { addExposedPorts(CONTROL_CENTER_PORT); controlCenterEnabled = true; return self(); } /** * Get the mapped port for the MQTT port of the container. *

* Must be called after the container is started. * * @return the port on the host machine for mqtt clients to connect */ public int getMqttPort() { return this.getMappedPort(MQTT_PORT); } private @NotNull MountableFile cloneWithFileMode(final @NotNull MountableFile mountableFile) { return MountableFile.forHostPath(mountableFile.getResolvedPath(), HiveMQContainer.MODE); } } ================================================ FILE: modules/hivemq/src/main/java/org/testcontainers/hivemq/HiveMQExtension.java ================================================ package org.testcontainers.hivemq; import lombok.Getter; import org.apache.commons.io.FileUtils; import org.jboss.shrinkwrap.api.ExtensionLoader; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.exporter.ZipExporter; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.jboss.shrinkwrap.impl.base.exporter.zip.ZipExporterImpl; import org.jboss.shrinkwrap.impl.base.spec.JavaArchiveImpl; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.containers.ContainerLaunchException; import java.io.File; import java.nio.charset.Charset; import java.nio.file.Files; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Set; import javassist.ClassPool; import javassist.NotFoundException; public class HiveMQExtension { private static final String VALID_EXTENSION_XML = "" + // " %s" + // " %s" + // " %s" + // " %s" + // " %s" + // ""; private static final String EXTENSION_MAIN_CLASS_NAME = "com.hivemq.extension.sdk.api.ExtensionMain"; private static final Logger LOGGER = LoggerFactory.getLogger(HiveMQExtension.class); @Getter @NotNull private final String id; @Getter @NotNull private final String name; @Getter @NotNull private final String version; @Getter private final int priority; @Getter private final int startPriority; @Getter private final boolean disabledOnStartup; @Getter @NotNull private final Class mainClass; @NotNull private final List> additionalClasses; private HiveMQExtension( final @NotNull String id, final @NotNull String name, final @NotNull String version, final int priority, final int startPriority, final boolean disabledOnStartup, final @NotNull Class mainClass, final @NotNull List> additionalClasses ) { this.id = id; this.name = name; this.version = version; this.priority = priority; this.startPriority = startPriority; this.disabledOnStartup = disabledOnStartup; this.mainClass = mainClass; this.additionalClasses = additionalClasses; } @NotNull File createExtension(final @NotNull HiveMQExtension hiveMQExtension) throws Exception { final File tempDir = Files.createTempDirectory("").toFile(); final File extensionDir = new File(tempDir, hiveMQExtension.getId()); FileUtils.writeStringToFile( new File(extensionDir, "hivemq-extension.xml"), String.format( VALID_EXTENSION_XML, hiveMQExtension.getId(), hiveMQExtension.getName(), hiveMQExtension.getVersion(), hiveMQExtension.getPriority(), hiveMQExtension.getStartPriority() ), Charset.defaultCharset() ); if (hiveMQExtension.isDisabledOnStartup()) { final File disabled = new File(extensionDir, "DISABLED"); final boolean newFile = disabled.createNewFile(); if (!newFile) { throw new ContainerLaunchException( "Could not create DISABLED file '" + disabled.getAbsolutePath() + "' on host machine." ); } } // Shadow Gradle plugin doesn't know how to handle ShrinkWrap's SPI definitions // This workaround creates the mappings programmatically // TODO write a custom Gradle Shadow transformer? ExtensionLoader extensionLoader = ShrinkWrap.getDefaultDomain().getConfiguration().getExtensionLoader(); extensionLoader.addOverride(JavaArchive.class, JavaArchiveImpl.class); extensionLoader.addOverride(ZipExporter.class, ZipExporterImpl.class); final JavaArchive javaArchive = ShrinkWrap .create(JavaArchive.class) .addAsServiceProvider(EXTENSION_MAIN_CLASS_NAME, hiveMQExtension.getMainClass().getName()); putSubclassesIntoJar(hiveMQExtension.getId(), hiveMQExtension.getMainClass(), javaArchive); for (final Class additionalClass : hiveMQExtension.getAdditionalClasses()) { javaArchive.addClass(additionalClass); putSubclassesIntoJar(hiveMQExtension.getId(), additionalClass, javaArchive); } javaArchive.as(ZipExporter.class).exportTo(new File(extensionDir, "extension.jar")); return extensionDir; } private void putSubclassesIntoJar( final @NotNull String extensionId, final @Nullable Class clazz, final @NotNull JavaArchive javaArchive ) throws NotFoundException { if (clazz != null) { final Set subClassNames = ClassPool .getDefault() .get(clazz.getName()) .getClassFile() .getConstPool() .getClassNames(); for (final String subClassName : subClassNames) { final String className = subClassName.replaceAll("/", "."); if (!className.startsWith("[L")) { LOGGER.debug("Trying to package subclass '{}' into extension '{}'.", className, extensionId); javaArchive.addClass(className); } else { LOGGER.debug("Class '{}' will be ignored.", className); } } } } public @NotNull List> getAdditionalClasses() { return Collections.unmodifiableList(additionalClasses); } public static @NotNull Builder builder() { return new Builder(); } public static final class Builder { @Nullable private String id; @Nullable private String name; @Nullable private String version; private int priority = 0; private int startPriority = 0; private boolean disabledOnStartup = false; @Nullable private Class mainClass; @NotNull private final LinkedList> additionalClasses = new LinkedList<>(); /** * Builds the {@link HiveMQExtension} with the provided values or default values. * @return the HiveMQ Extension */ public @NotNull HiveMQExtension build() { if (id == null || id.isEmpty()) { throw new IllegalArgumentException("extension id must not be null or empty"); } if (name == null || name.isEmpty()) { throw new IllegalArgumentException("extension name must not be null or empty"); } if (version == null || version.isEmpty()) { throw new IllegalArgumentException("extension version must not be null or empty"); } if (mainClass == null) { throw new IllegalArgumentException("extension main class must not be null"); } return new HiveMQExtension( id, name, version, priority, startPriority, disabledOnStartup, mainClass, additionalClasses ); } /** * Sets the identifier of the {@link HiveMQExtension}. * * @param id the identifier, must not be empty * @return the {@link Builder} */ public @NotNull Builder id(final @NotNull String id) { this.id = id; return this; } /** * Sets the name of the {@link HiveMQExtension}. * * @param name the identifier, must not be empty * @return the {@link Builder} */ public @NotNull Builder name(final @NotNull String name) { this.name = name; return this; } /** * Sets the version of the {@link HiveMQExtension}. * * @param version the version, must not be empty * @return the {@link Builder} */ public @NotNull Builder version(final @NotNull String version) { this.version = version; return this; } /** * Sets the priority of the {@link HiveMQExtension}. * * @param priority the priority * @return the {@link Builder} */ public @NotNull Builder priority(final int priority) { this.priority = priority; return this; } /** * Sets the start-priority of the {@link HiveMQExtension}. * * @param startPriority the start-priority * @return the {@link Builder} */ public @NotNull Builder startPriority(final int startPriority) { this.startPriority = startPriority; return this; } /** * Flag, that indicates whether the {@link HiveMQExtension} should be disabled when HiveMQ starts. * Disabling on startup is achieved by placing a DISABLED file in the {@link HiveMQExtension}'s directory before coping it to the container. * * @param disabledOnStartup if the {@link HiveMQExtension} should be disabled when HiveMQ starts * @return the {@link Builder} */ public @NotNull Builder disabledOnStartup(final boolean disabledOnStartup) { this.disabledOnStartup = disabledOnStartup; return this; } /** * The main class of the {@link HiveMQExtension}. * This class MUST implement com.hivemq.extension.sdk.api.ExtensionMain. * * @param mainClass the main class * @return the {@link Builder} * @throws IllegalArgumentException if the provides class does not implement com.hivemq.extension.sdk.api.ExtensionMain} * @throws IllegalStateException if com.hivemq.extension.sdk.api.ExtensionMain is not found in the classpath */ public @NotNull Builder mainClass(final @NotNull Class mainClass) { try { final Class extensionMain = Class.forName(EXTENSION_MAIN_CLASS_NAME); if (!extensionMain.isAssignableFrom(mainClass)) { throw new IllegalArgumentException( "The provided class does not implement '" + EXTENSION_MAIN_CLASS_NAME + "'" ); } this.mainClass = mainClass; return this; } catch (final ClassNotFoundException e) { throw new IllegalStateException( "The class '" + EXTENSION_MAIN_CLASS_NAME + "' was not found in the classpath." ); } } /** * Adds an additional class to the .jar file of the {@link HiveMQExtension}. * * @param clazz the additional class * @return the {@link Builder} */ public @NotNull Builder addAdditionalClass(final @NotNull Class clazz) { this.additionalClasses.add(clazz); return this; } } } ================================================ FILE: modules/hivemq/src/main/java/org/testcontainers/hivemq/PathUtil.java ================================================ package org.testcontainers.hivemq; import org.jetbrains.annotations.NotNull; class PathUtil { static @NotNull String prepareInnerPath(@NotNull String innerPath) { if ("/".equals(innerPath) || innerPath.isEmpty()) { return "/"; } if (!innerPath.startsWith("/")) { innerPath = "/" + innerPath; } if (!innerPath.endsWith("/")) { innerPath += "/"; } return innerPath; } static @NotNull String prepareAppendPath(@NotNull String appendPath) { if (!appendPath.startsWith("/")) { appendPath = "/" + appendPath; } return appendPath; } } ================================================ FILE: modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithControlCenterIT.java ================================================ package org.testcontainers.hivemq; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.testcontainers.utility.DockerImageName; import java.util.concurrent.TimeUnit; class ContainerWithControlCenterIT { public static final int CONTROL_CENTER_PORT = 8080; @Test @Timeout(value = 3, unit = TimeUnit.MINUTES) void test() throws Exception { try ( final HiveMQContainer hivemq = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq4").withTag("4.7.4")) .withControlCenter() ) { hivemq.start(); try (final CloseableHttpClient httpClient = HttpClientBuilder.create().build()) { final HttpUriRequest request = new HttpGet( "http://" + hivemq.getHost() + ":" + hivemq.getMappedPort(CONTROL_CENTER_PORT) ); httpClient.execute(request); } } } } ================================================ FILE: modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithCustomConfigIT.java ================================================ package org.testcontainers.hivemq; import com.hivemq.client.mqtt.datatypes.MqttQos; import com.hivemq.client.mqtt.exceptions.MqttSessionExpiredException; import com.hivemq.client.mqtt.mqtt5.Mqtt5BlockingClient; import com.hivemq.client.mqtt.mqtt5.Mqtt5Client; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; class ContainerWithCustomConfigIT { @Test @Timeout(value = 3, unit = TimeUnit.MINUTES) void test() throws Exception { try ( final HiveMQContainer hivemq = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq4").withTag("4.7.4")) .withHiveMQConfig(MountableFile.forClasspathResource("/config.xml")) ) { hivemq.start(); final Mqtt5BlockingClient publisher = Mqtt5Client .builder() .identifier("publisher") .serverPort(hivemq.getMqttPort()) .serverHost(hivemq.getHost()) .buildBlocking(); publisher.connect(); assertThatExceptionOfType(MqttSessionExpiredException.class) .isThrownBy(() -> { // this should fail since only QoS 0 is allowed by the configuration publisher.publishWith().topic("test/topic").qos(MqttQos.EXACTLY_ONCE).send(); }); } } } ================================================ FILE: modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionFromDirectoryIT.java ================================================ package org.testcontainers.hivemq; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.slf4j.event.Level; import org.testcontainers.hivemq.util.TestPublishModifiedUtil; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.util.concurrent.TimeUnit; class ContainerWithExtensionFromDirectoryIT { @ParameterizedTest @ValueSource( strings = { "2020.1", // first version that provided a container image "2024.3", // version that runs the image as a non-root user by default } ) @Timeout(value = 3, unit = TimeUnit.MINUTES) void test(final @NotNull String hivemqCeTag) throws Exception { try ( final HiveMQContainer hivemq = new HiveMQContainer( DockerImageName.parse("hivemq/hivemq-ce").withTag(hivemqCeTag) ) .withExtension(MountableFile.forClasspathResource("/modifier-extension")) .waitForExtension("Modifier Extension") .withHiveMQConfig(MountableFile.forClasspathResource("/inMemoryConfig.xml")) .withLogLevel(Level.DEBUG) ) { hivemq.start(); TestPublishModifiedUtil.testPublishModified(hivemq.getMqttPort(), hivemq.getHost()); } } @Test @Timeout(value = 3, unit = TimeUnit.MINUTES) void test_wrongDirectoryName() throws Exception { try ( final HiveMQContainer hivemq = new HiveMQContainer( DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3") ) .withExtension(MountableFile.forClasspathResource("/modifier-extension-wrong-name")) .waitForExtension("Modifier Extension") .withLogLevel(Level.DEBUG) ) { hivemq.start(); TestPublishModifiedUtil.testPublishModified(hivemq.getMqttPort(), hivemq.getHost()); } } } ================================================ FILE: modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionIT.java ================================================ package org.testcontainers.hivemq; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.testcontainers.hivemq.util.MyExtension; import org.testcontainers.hivemq.util.TestPublishModifiedUtil; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.util.concurrent.TimeUnit; class ContainerWithExtensionIT { @ParameterizedTest @ValueSource( strings = { "2020.1", // first version that provided a container image "2024.3", // version that runs the image as a non-root user by default } ) @Timeout(value = 3, unit = TimeUnit.MINUTES) void test(final @NotNull String hivemqCeTag) throws Exception { final HiveMQExtension hiveMQExtension = HiveMQExtension .builder() .id("extension-1") .name("my-extension") .version("1.0") .mainClass(MyExtension.class) .build(); try ( final HiveMQContainer hivemq = new HiveMQContainer( DockerImageName.parse("hivemq/hivemq-ce").withTag(hivemqCeTag) ) .withHiveMQConfig(MountableFile.forClasspathResource("/inMemoryConfig.xml")) .waitForExtension(hiveMQExtension) .withExtension(hiveMQExtension) ) { hivemq.start(); TestPublishModifiedUtil.testPublishModified(hivemq.getMqttPort(), hivemq.getHost()); hivemq.stop(); hivemq.start(); TestPublishModifiedUtil.testPublishModified(hivemq.getMqttPort(), hivemq.getHost()); hivemq.stop(); hivemq.start(); TestPublishModifiedUtil.testPublishModified(hivemq.getMqttPort(), hivemq.getHost()); } } } ================================================ FILE: modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionSubclassIT.java ================================================ package org.testcontainers.hivemq; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.slf4j.event.Level; import org.testcontainers.hivemq.util.MyExtensionWithSubclasses; import org.testcontainers.hivemq.util.TestPublishModifiedUtil; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.util.concurrent.TimeUnit; class ContainerWithExtensionSubclassIT { @Test @Timeout(value = 3, unit = TimeUnit.MINUTES) void test() throws Exception { final HiveMQExtension hiveMQExtension = HiveMQExtension .builder() .id("extension-1") .name("my-extension") .version("1.0") .mainClass(MyExtensionWithSubclasses.class) .build(); try ( final HiveMQContainer hivemq = new HiveMQContainer( DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3") ) .waitForExtension(hiveMQExtension) .withExtension(hiveMQExtension) .withHiveMQConfig(MountableFile.forClasspathResource("/inMemoryConfig.xml")) .withLogLevel(Level.DEBUG) ) { hivemq.start(); TestPublishModifiedUtil.testPublishModified(hivemq.getMqttPort(), hivemq.getHost()); } } } ================================================ FILE: modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithFileInExtensionHomeIT.java ================================================ package org.testcontainers.hivemq; import com.hivemq.extension.sdk.api.ExtensionMain; import com.hivemq.extension.sdk.api.interceptor.publish.PublishInboundInterceptor; import com.hivemq.extension.sdk.api.parameter.ExtensionStartInput; import com.hivemq.extension.sdk.api.parameter.ExtensionStartOutput; import com.hivemq.extension.sdk.api.parameter.ExtensionStopInput; import com.hivemq.extension.sdk.api.parameter.ExtensionStopOutput; import com.hivemq.extension.sdk.api.services.Services; import com.hivemq.extension.sdk.api.services.intializer.ClientInitializer; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.testcontainers.hivemq.util.TestPublishModifiedUtil; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.io.File; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; class ContainerWithFileInExtensionHomeIT { @ParameterizedTest @ValueSource( strings = { "2020.1", // first version that provided a container image "2024.3", // version that runs the image as a non-root user by default } ) @Timeout(value = 3, unit = TimeUnit.MINUTES) void test(final @NotNull String hivemqCeTag) throws Exception { final HiveMQExtension hiveMQExtension = HiveMQExtension .builder() .id("extension-1") .name("my-extension") .version("1.0") .mainClass(FileCheckerExtension.class) .build(); try ( final HiveMQContainer hivemq = new HiveMQContainer( DockerImageName.parse("hivemq/hivemq-ce").withTag(hivemqCeTag) ) .withHiveMQConfig(MountableFile.forClasspathResource("/inMemoryConfig.xml")) .withExtension(hiveMQExtension) .waitForExtension(hiveMQExtension) .withFileInExtensionHomeFolder( MountableFile.forClasspathResource("/additionalFile.txt"), "extension-1", "/additionalFiles/my-file.txt" ) ) { hivemq.start(); TestPublishModifiedUtil.testPublishModified(hivemq.getMqttPort(), hivemq.getHost()); } } public static class FileCheckerExtension implements ExtensionMain { @Override public void extensionStart( final @NotNull ExtensionStartInput extensionStartInput, final @NotNull ExtensionStartOutput extensionStartOutput ) { final PublishInboundInterceptor publishInboundInterceptor = (publishInboundInput, publishInboundOutput) -> { final File extensionHomeFolder = extensionStartInput.getExtensionInformation().getExtensionHomeFolder(); final File additionalFile = new File(extensionHomeFolder, "additionalFiles/my-file.txt"); if (additionalFile.exists()) { publishInboundOutput .getPublishPacket() .setPayload(ByteBuffer.wrap("modified".getBytes(StandardCharsets.UTF_8))); } }; final ClientInitializer clientInitializer = (initializerInput, clientContext) -> { clientContext.addPublishInboundInterceptor(publishInboundInterceptor); }; Services.initializerRegistry().setClientInitializer(clientInitializer); } @Override public void extensionStop( final @NotNull ExtensionStopInput extensionStopInput, final @NotNull ExtensionStopOutput extensionStopOutput ) {} } } ================================================ FILE: modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithFileInHomeIT.java ================================================ package org.testcontainers.hivemq; import com.hivemq.extension.sdk.api.ExtensionMain; import com.hivemq.extension.sdk.api.interceptor.publish.PublishInboundInterceptor; import com.hivemq.extension.sdk.api.parameter.ExtensionStartInput; import com.hivemq.extension.sdk.api.parameter.ExtensionStartOutput; import com.hivemq.extension.sdk.api.parameter.ExtensionStopInput; import com.hivemq.extension.sdk.api.parameter.ExtensionStopOutput; import com.hivemq.extension.sdk.api.services.Services; import com.hivemq.extension.sdk.api.services.intializer.ClientInitializer; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.testcontainers.hivemq.util.TestPublishModifiedUtil; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.io.File; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; class ContainerWithFileInHomeIT { @Test @Timeout(value = 3, unit = TimeUnit.MINUTES) void test() throws Exception { final HiveMQExtension hiveMQExtension = HiveMQExtension .builder() .id("extension-1") .name("my-extension") .version("1.0") .mainClass(FileCheckerExtension.class) .build(); try ( final HiveMQContainer hivemq = new HiveMQContainer( DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3") ) .withHiveMQConfig(MountableFile.forClasspathResource("/inMemoryConfig.xml")) .withExtension(hiveMQExtension) .waitForExtension(hiveMQExtension) .withFileInHomeFolder( MountableFile.forClasspathResource("/additionalFile.txt"), "/additionalFiles/my-file.txt" ) ) { hivemq.start(); TestPublishModifiedUtil.testPublishModified(hivemq.getMqttPort(), hivemq.getHost()); } } public static class FileCheckerExtension implements ExtensionMain { @Override public void extensionStart( @NotNull ExtensionStartInput extensionStartInput, @NotNull ExtensionStartOutput extensionStartOutput ) { final PublishInboundInterceptor publishInboundInterceptor = (publishInboundInput, publishInboundOutput) -> { final File homeFolder = extensionStartInput.getServerInformation().getHomeFolder(); final File additionalFile = new File(homeFolder, "additionalFiles/my-file.txt"); if (additionalFile.exists()) { publishInboundOutput .getPublishPacket() .setPayload(ByteBuffer.wrap("modified".getBytes(StandardCharsets.UTF_8))); } }; final ClientInitializer clientInitializer = (initializerInput, clientContext) -> { clientContext.addPublishInboundInterceptor(publishInboundInterceptor); }; Services.initializerRegistry().setClientInitializer(clientInitializer); } @Override public void extensionStop( @NotNull ExtensionStopInput extensionStopInput, @NotNull ExtensionStopOutput extensionStopOutput ) {} } } ================================================ FILE: modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithLicenseIT.java ================================================ package org.testcontainers.hivemq; import com.hivemq.extension.sdk.api.ExtensionMain; import com.hivemq.extension.sdk.api.interceptor.publish.PublishInboundInterceptor; import com.hivemq.extension.sdk.api.parameter.ExtensionStartInput; import com.hivemq.extension.sdk.api.parameter.ExtensionStartOutput; import com.hivemq.extension.sdk.api.parameter.ExtensionStopInput; import com.hivemq.extension.sdk.api.parameter.ExtensionStopOutput; import com.hivemq.extension.sdk.api.services.Services; import com.hivemq.extension.sdk.api.services.intializer.ClientInitializer; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.testcontainers.hivemq.util.TestPublishModifiedUtil; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.io.File; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; class ContainerWithLicenseIT { @Test @Timeout(value = 3, unit = TimeUnit.MINUTES) void test() throws Exception { final HiveMQExtension hiveMQExtension = HiveMQExtension .builder() .id("extension-1") .name("my-extension") .version("1.0") .mainClass(LicenceCheckerExtension.class) .build(); try ( final HiveMQContainer hivemq = new HiveMQContainer( DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3") ) .withHiveMQConfig(MountableFile.forClasspathResource("/inMemoryConfig.xml")) .withExtension(hiveMQExtension) .waitForExtension(hiveMQExtension) .withLicense(MountableFile.forClasspathResource("/myLicense.lic")) .withLicense(MountableFile.forClasspathResource("/myExtensionLicense.elic")) ) { hivemq.start(); TestPublishModifiedUtil.testPublishModified(hivemq.getMqttPort(), hivemq.getHost()); } } @SuppressWarnings("CodeBlock2Expr") public static class LicenceCheckerExtension implements ExtensionMain { @Override public void extensionStart( @NotNull ExtensionStartInput extensionStartInput, @NotNull ExtensionStartOutput extensionStartOutput ) { final PublishInboundInterceptor publishInboundInterceptor = (publishInboundInput, publishInboundOutput) -> { final File homeFolder = extensionStartInput.getServerInformation().getHomeFolder(); final File myLicence = new File(homeFolder, "license/myLicense.lic"); final File myExtensionLicence = new File(homeFolder, "license/myExtensionLicense.elic"); if (myLicence.exists() && myExtensionLicence.exists()) { publishInboundOutput .getPublishPacket() .setPayload(ByteBuffer.wrap("modified".getBytes(StandardCharsets.UTF_8))); } }; final ClientInitializer clientInitializer = (initializerInput, clientContext) -> { clientContext.addPublishInboundInterceptor(publishInboundInterceptor); }; Services.initializerRegistry().setClientInitializer(clientInitializer); } @Override public void extensionStop( @NotNull ExtensionStopInput extensionStopInput, @NotNull ExtensionStopOutput extensionStopOutput ) {} } } ================================================ FILE: modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithoutPlatformExtensionsIT.java ================================================ package org.testcontainers.hivemq; import com.hivemq.client.mqtt.MqttClient; import com.hivemq.client.mqtt.MqttGlobalPublishFilter; import com.hivemq.client.mqtt.mqtt5.Mqtt5BlockingClient; import com.hivemq.client.mqtt.mqtt5.message.publish.Mqtt5Publish; import com.hivemq.extension.sdk.api.ExtensionMain; import com.hivemq.extension.sdk.api.auth.SimpleAuthenticator; import com.hivemq.extension.sdk.api.parameter.ExtensionStartInput; import com.hivemq.extension.sdk.api.parameter.ExtensionStartOutput; import com.hivemq.extension.sdk.api.parameter.ExtensionStopInput; import com.hivemq.extension.sdk.api.parameter.ExtensionStopOutput; import com.hivemq.extension.sdk.api.services.Services; import com.hivemq.extension.sdk.api.services.builder.Builders; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.testcontainers.utility.DockerImageName; import java.io.File; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; class ContainerWithoutPlatformExtensionsIT { @NotNull private final HiveMQExtension hiveMQExtension = HiveMQExtension .builder() .name("MyExtension") .id("my-extension") .version("1.0.0") .mainClass(CheckerExtension.class) .build(); @Test @Timeout(value = 3, unit = TimeUnit.MINUTES) void removeAllPlatformExtensions() throws InterruptedException { try ( final HiveMQContainer hivemq = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq4").withTag("4.7.4")) .withExtension(hiveMQExtension) .waitForExtension(hiveMQExtension) .withoutPrepackagedExtensions() ) { hivemq.start(); final Mqtt5BlockingClient client = MqttClient .builder() .serverPort(hivemq.getMqttPort()) .serverHost(hivemq.getHost()) .useMqttVersion5() .buildBlocking(); client.connect(); final Mqtt5BlockingClient.Mqtt5Publishes publishes = client.publishes(MqttGlobalPublishFilter.ALL); client.subscribeWith().topicFilter("extensions").send(); final Mqtt5Publish receive = publishes.receive(); assertThat(receive.getPayload()).isPresent(); final String extensionInfo = new String(receive.getPayloadAsBytes()); assertThat(extensionInfo).doesNotContain("hivemq-allow-all-extension"); assertThat(extensionInfo).doesNotContain("hivemq-kafka-extension"); assertThat(extensionInfo).doesNotContain("hivemq-bridge-extension"); assertThat(extensionInfo).doesNotContain("hivemq-enterprise-security-extension"); hivemq.start(); } } @Test @Timeout(value = 3, unit = TimeUnit.MINUTES) void removeKafkaExtension() throws InterruptedException { try ( final HiveMQContainer hivemq = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq4").withTag("4.7.4")) .withExtension(hiveMQExtension) .waitForExtension(hiveMQExtension) .withoutPrepackagedExtensions("hivemq-kafka-extension") ) { hivemq.start(); final Mqtt5BlockingClient client = MqttClient .builder() .serverPort(hivemq.getMqttPort()) .serverHost(hivemq.getHost()) .useMqttVersion5() .buildBlocking(); client.connect(); final Mqtt5BlockingClient.Mqtt5Publishes publishes = client.publishes(MqttGlobalPublishFilter.ALL); client.subscribeWith().topicFilter("extensions").send(); final Mqtt5Publish receive = publishes.receive(); assertThat(receive.getPayload().isPresent()).isTrue(); final String extensionInfo = new String(receive.getPayloadAsBytes()); assertThat(extensionInfo).contains("hivemq-allow-all-extension"); assertThat(extensionInfo).doesNotContain("hivemq-kafka-extension"); assertThat(extensionInfo).contains("hivemq-bridge-extension"); assertThat(extensionInfo).contains("hivemq-enterprise-security-extension"); } } public static class CheckerExtension implements ExtensionMain { @Override public void extensionStart( final @NotNull ExtensionStartInput extensionStartInput, final @NotNull ExtensionStartOutput extensionStartOutput ) { final String extensionFolders = Arrays .stream(extensionStartInput.getServerInformation().getExtensionsFolder().listFiles()) .filter(File::isDirectory) .map(File::getName) .collect(Collectors.joining("\n")); final byte[] bytes = extensionFolders.getBytes(StandardCharsets.UTF_8); Services .publishService() .publish(Builders.publish().topic("extensions").retain(true).payload(ByteBuffer.wrap(bytes)).build()); Services .securityRegistry() .setAuthenticatorProvider(authenticatorProviderInput -> { return (SimpleAuthenticator) (simpleAuthInput, simpleAuthOutput) -> { simpleAuthOutput.authenticateSuccessfully(); }; }); } @Override public void extensionStop( final @NotNull ExtensionStopInput extensionStopInput, final @NotNull ExtensionStopOutput extensionStopOutput ) {} } } ================================================ FILE: modules/hivemq/src/test/java/org/testcontainers/hivemq/CreateFileInCopiedDirectoryIT.java ================================================ package org.testcontainers.hivemq; import com.hivemq.extension.sdk.api.ExtensionMain; import com.hivemq.extension.sdk.api.interceptor.publish.PublishInboundInterceptor; import com.hivemq.extension.sdk.api.parameter.ExtensionStartInput; import com.hivemq.extension.sdk.api.parameter.ExtensionStartOutput; import com.hivemq.extension.sdk.api.parameter.ExtensionStopInput; import com.hivemq.extension.sdk.api.parameter.ExtensionStopOutput; import com.hivemq.extension.sdk.api.services.Services; import com.hivemq.extension.sdk.api.services.intializer.ClientInitializer; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.testcontainers.hivemq.util.TestPublishModifiedUtil; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; class CreateFileInCopiedDirectoryIT { private @NotNull MountableFile createDirectory() throws IOException { final File directory = new File(Files.createTempDirectory("").toFile(), "directory"); assertThat(directory.mkdir()).isTrue(); final File subdirectory = new File(directory, "sub-directory"); assertThat(subdirectory.mkdir()).isTrue(); return MountableFile.forHostPath(directory.getPath()); } @Test @Timeout(value = 3, unit = TimeUnit.MINUTES) void test() throws Exception { final HiveMQExtension extension = HiveMQExtension .builder() .id("extension-1") .name("my-extension") .version("1.0") .mainClass(FileCreatorExtension.class) .build(); try ( final HiveMQContainer hivemq = new HiveMQContainer( DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3") ) .withHiveMQConfig(MountableFile.forClasspathResource("/inMemoryConfig.xml")) .withExtension(extension) .waitForExtension(extension) .withFileInHomeFolder(createDirectory(), "directory") ) { hivemq.start(); TestPublishModifiedUtil.testPublishModified(hivemq.getMqttPort(), hivemq.getHost()); } } public static class FileCreatorExtension implements ExtensionMain { @Override public void extensionStart( @NotNull ExtensionStartInput extensionStartInput, @NotNull ExtensionStartOutput extensionStartOutput ) { final PublishInboundInterceptor publishInboundInterceptor = (publishInboundInput, publishInboundOutput) -> { final File homeFolder = extensionStartInput.getServerInformation().getHomeFolder(); final File dir = new File(homeFolder, "directory"); final File dirFile = new File(dir, "file.txt"); final File subDir = new File(dir, "sub-directory"); final File subDirFile = new File(subDir, "file.txt"); try { if (dirFile.createNewFile() && subDirFile.createNewFile()) { publishInboundOutput .getPublishPacket() .setPayload(ByteBuffer.wrap("modified".getBytes(StandardCharsets.UTF_8))); } } catch (IOException e) { e.printStackTrace(); } }; final ClientInitializer clientInitializer = (initializerInput, clientContext) -> { clientContext.addPublishInboundInterceptor(publishInboundInterceptor); }; Services.initializerRegistry().setClientInitializer(clientInitializer); } @Override public void extensionStop( @NotNull ExtensionStopInput extensionStopInput, @NotNull ExtensionStopOutput extensionStopOutput ) {} } } ================================================ FILE: modules/hivemq/src/test/java/org/testcontainers/hivemq/CreateFileInExtensionDirectoryIT.java ================================================ package org.testcontainers.hivemq; import com.hivemq.extension.sdk.api.ExtensionMain; import com.hivemq.extension.sdk.api.interceptor.publish.PublishInboundInterceptor; import com.hivemq.extension.sdk.api.parameter.ExtensionStartInput; import com.hivemq.extension.sdk.api.parameter.ExtensionStartOutput; import com.hivemq.extension.sdk.api.parameter.ExtensionStopInput; import com.hivemq.extension.sdk.api.parameter.ExtensionStopOutput; import com.hivemq.extension.sdk.api.services.Services; import com.hivemq.extension.sdk.api.services.intializer.ClientInitializer; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.testcontainers.hivemq.util.TestPublishModifiedUtil; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; class CreateFileInExtensionDirectoryIT { @ParameterizedTest @ValueSource( strings = { "2020.1", // first version that provided a container image "2024.3", // version that runs the image as a non-root user by default } ) @Timeout(value = 3, unit = TimeUnit.MINUTES) void test(final @NotNull String hivemqCeTag) throws Exception { final HiveMQExtension hiveMQExtension = HiveMQExtension .builder() .id("extension-1") .name("my-extension") .version("1.0") .mainClass(FileCreatorExtension.class) .build(); try ( final HiveMQContainer hivemq = new HiveMQContainer( DockerImageName.parse("hivemq/hivemq-ce").withTag(hivemqCeTag) ) .withHiveMQConfig(MountableFile.forClasspathResource("/inMemoryConfig.xml")) .waitForExtension(hiveMQExtension) .withExtension(hiveMQExtension) ) { hivemq.start(); TestPublishModifiedUtil.testPublishModified(hivemq.getMqttPort(), hivemq.getHost()); } } public static class FileCreatorExtension implements ExtensionMain { @Override public void extensionStart( @NotNull ExtensionStartInput extensionStartInput, @NotNull ExtensionStartOutput extensionStartOutput ) { final PublishInboundInterceptor publishInboundInterceptor = (publishInboundInput, publishInboundOutput) -> { final File extensionHomeFolder = extensionStartInput.getExtensionInformation().getExtensionHomeFolder(); final File file = new File(extensionHomeFolder, "myfile.txt"); try { if (file.createNewFile()) { publishInboundOutput .getPublishPacket() .setPayload(ByteBuffer.wrap("modified".getBytes(StandardCharsets.UTF_8))); } } catch (IOException e) { e.printStackTrace(); } }; final ClientInitializer clientInitializer = (initializerInput, clientContext) -> { clientContext.addPublishInboundInterceptor(publishInboundInterceptor); }; Services.initializerRegistry().setClientInitializer(clientInitializer); } @Override public void extensionStop( @NotNull ExtensionStopInput extensionStopInput, @NotNull ExtensionStopOutput extensionStopOutput ) {} } } ================================================ FILE: modules/hivemq/src/test/java/org/testcontainers/hivemq/DisableEnableExtensionFromDirectoryIT.java ================================================ package org.testcontainers.hivemq; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.slf4j.event.Level; import org.testcontainers.hivemq.util.TestPublishModifiedUtil; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; class DisableEnableExtensionFromDirectoryIT { @Test @Timeout(value = 3, unit = TimeUnit.MINUTES) void test() throws Exception { try ( final HiveMQContainer hivemq = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq4").withTag("4.7.4")) .withExtension(MountableFile.forClasspathResource("/modifier-extension")) .waitForExtension("Modifier Extension") .withLogLevel(Level.DEBUG) ) { hivemq.start(); TestPublishModifiedUtil.testPublishModified(hivemq.getMqttPort(), hivemq.getHost()); hivemq.disableExtension("Modifier Extension", "modifier-extension"); assertThatExceptionOfType(ExecutionException.class) .isThrownBy(() -> TestPublishModifiedUtil.testPublishModified(hivemq.getMqttPort(), hivemq.getHost())); hivemq.enableExtension("Modifier Extension", "modifier-extension"); TestPublishModifiedUtil.testPublishModified(hivemq.getMqttPort(), hivemq.getHost()); } } } ================================================ FILE: modules/hivemq/src/test/java/org/testcontainers/hivemq/DisableEnableExtensionIT.java ================================================ package org.testcontainers.hivemq; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.slf4j.event.Level; import org.testcontainers.hivemq.util.MyExtension; import org.testcontainers.hivemq.util.TestPublishModifiedUtil; import org.testcontainers.utility.DockerImageName; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; class DisableEnableExtensionIT { @NotNull private final HiveMQExtension hiveMQExtension = HiveMQExtension .builder() .id("extension-1") .name("my-extension") .version("1.0") .disabledOnStartup(true) .mainClass(MyExtension.class) .build(); @Test @Timeout(value = 3, unit = TimeUnit.MINUTES) void test() throws Exception { try ( final HiveMQContainer hivemq = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq4").withTag("4.7.4")) .withExtension(hiveMQExtension) .withLogLevel(Level.DEBUG) ) { hivemq.start(); assertThatExceptionOfType(ExecutionException.class) .isThrownBy(() -> TestPublishModifiedUtil.testPublishModified(hivemq.getMqttPort(), hivemq.getHost())); hivemq.enableExtension(hiveMQExtension); TestPublishModifiedUtil.testPublishModified(hivemq.getMqttPort(), hivemq.getHost()); hivemq.disableExtension(hiveMQExtension); assertThatExceptionOfType(ExecutionException.class) .isThrownBy(() -> TestPublishModifiedUtil.testPublishModified(hivemq.getMqttPort(), hivemq.getHost())); hivemq.enableExtension(hiveMQExtension); TestPublishModifiedUtil.testPublishModified(hivemq.getMqttPort(), hivemq.getHost()); } } } ================================================ FILE: modules/hivemq/src/test/java/org/testcontainers/hivemq/HiveMQExtensionTest.java ================================================ package org.testcontainers.hivemq; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; class HiveMQExtensionTest { @Test void builder_classDoesNotImplementExtensionMain_exception() { assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> HiveMQExtension.builder().mainClass(HiveMQExtensionTest.class)); } } ================================================ FILE: modules/hivemq/src/test/java/org/testcontainers/hivemq/HiveMQTestContainerCore.java ================================================ package org.testcontainers.hivemq; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.io.File; import java.io.IOException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; class HiveMQTestContainerCore { @NotNull final HiveMQContainer container = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3")); @TempDir File tempDir; @Test void withExtension_fileDoesNotExist_Exception() { final MountableFile mountableFile = MountableFile.forHostPath("/this/does/not/exist"); assertThatExceptionOfType(ContainerLaunchException.class) .isThrownBy(() -> container.withExtension(mountableFile)); } @Test void withExtension_fileNoDirectory_Exception() throws IOException { final File extension = new File(tempDir, "extension"); assertThat(extension.createNewFile()).isTrue(); final MountableFile mountableFile = MountableFile.forHostPath(extension.getAbsolutePath()); assertThatExceptionOfType(ContainerLaunchException.class) .isThrownBy(() -> container.withExtension(mountableFile)); } @Test void withLicense_fileDoesNotExist_Exception() { final MountableFile mountableFile = MountableFile.forHostPath("/this/does/not/exist"); assertThatExceptionOfType(ContainerLaunchException.class) .isThrownBy(() -> container.withLicense(mountableFile)); } @Test void withExtension_fileEndingWrong_Exception() throws IOException { final File extension = new File(tempDir, "extension.wrong"); assertThat(extension.createNewFile()).isTrue(); final MountableFile mountableFile = MountableFile.forHostPath(extension.getAbsolutePath()); assertThatExceptionOfType(ContainerLaunchException.class) .isThrownBy(() -> container.withLicense(mountableFile)); } @Test void withHiveMQConfig_fileDoesNotExist_Exception() { final MountableFile mountableFile = MountableFile.forHostPath("/this/does/not/exist"); assertThatExceptionOfType(ContainerLaunchException.class) .isThrownBy(() -> container.withHiveMQConfig(mountableFile)); } @Test void withFileInHomeFolder_fileDoesNotExist_Exception() { final MountableFile mountableFile = MountableFile.forHostPath("/this/does/not/exist"); assertThatExceptionOfType(ContainerLaunchException.class) .isThrownBy(() -> container.withFileInHomeFolder(mountableFile, "some/path")); } @Test void withFileInHomeFolder_pathEmpty_Exception() { final MountableFile mountableFile = MountableFile.forHostPath("/this/does/not/exist"); assertThatExceptionOfType(ContainerLaunchException.class) .isThrownBy(() -> container.withFileInHomeFolder(mountableFile, "")); } @Test void withFileInExtensionHomeFolder_withPath_fileDoesNotExist_Exception() { final MountableFile mountableFile = MountableFile.forHostPath("/this/does/not/exist"); assertThatExceptionOfType(ContainerLaunchException.class) .isThrownBy(() -> container.withFileInExtensionHomeFolder(mountableFile, "my-extension", "some/path")); } @Test void withFileInExtensionHomeFolder_fileDoesNotExist_Exception() { final MountableFile mountableFile = MountableFile.forHostPath("/this/does/not/exist"); assertThatExceptionOfType(ContainerLaunchException.class) .isThrownBy(() -> { final HiveMQContainer hiveMQContainer = container.withFileInExtensionHomeFolder( mountableFile, "my-extension" ); }); } } ================================================ FILE: modules/hivemq/src/test/java/org/testcontainers/hivemq/PathUtilTest.java ================================================ package org.testcontainers.hivemq; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class PathUtilTest { @Test void prepareInnerPath_emptyString() { assertThat(PathUtil.prepareInnerPath("")).isEqualTo("/"); } @Test void prepareInnerPath_onlyDelimiter() { assertThat(PathUtil.prepareInnerPath("/")).isEqualTo("/"); } @Test void prepareInnerPath_noDelimiter() { assertThat(PathUtil.prepareInnerPath("path")).isEqualTo("/path/"); } @Test void prepareInnerPath_delimiterAtEnd() { assertThat(PathUtil.prepareInnerPath("path/")).isEqualTo("/path/"); } @Test void prepareInnerPath_delimiterAtBeginning() { assertThat(PathUtil.prepareInnerPath("/path")).isEqualTo("/path/"); } @Test void prepareAppendPath_delimiterAtBeginning_noChange() { assertThat(PathUtil.prepareAppendPath("/path")).isEqualTo("/path"); } @Test void prepareAppendPath_delimiterAtBeginning_delimiterAdded() { assertThat(PathUtil.prepareAppendPath("path")).isEqualTo("/path"); } } ================================================ FILE: modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoDisableExtensionsIT.java ================================================ package org.testcontainers.hivemq.docs; import org.junit.jupiter.api.Test; import org.testcontainers.hivemq.HiveMQContainer; import org.testcontainers.hivemq.HiveMQExtension; import org.testcontainers.hivemq.util.MyExtension; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; @Testcontainers class DemoDisableExtensionsIT { // noExtensions { @Container final HiveMQContainer hivemqNoExtensions = new HiveMQContainer( DockerImageName.parse("hivemq/hivemq4").withTag("4.7.4") ) .withoutPrepackagedExtensions(); // } // noKafkaExtension { @Container final HiveMQContainer hivemqNoKafkaExtension = new HiveMQContainer( DockerImageName.parse("hivemq/hivemq4").withTag("4.7.4") ) .withoutPrepackagedExtensions("hivemq-kafka-extension"); // } // startDisabled { private final HiveMQExtension hiveMQExtension = HiveMQExtension .builder() .id("extension-1") .name("my-extension") .version("1.0") .disabledOnStartup(true) .mainClass(MyExtension.class) .build(); @Container final HiveMQContainer hivemq = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq4").withTag("4.7.4")) .withExtension(hiveMQExtension); // } // startFromFilesystem { @Container final HiveMQContainer hivemqExtensionFromFilesystem = new HiveMQContainer( DockerImageName.parse("hivemq/hivemq4").withTag("4.7.4") ) .withExtension(MountableFile.forHostPath("src/test/resources/modifier-extension")); // } // hiveRuntimeEnable { @Test void test_disable_enable_extension() throws Exception { hivemq.enableExtension(hiveMQExtension); hivemq.disableExtension(hiveMQExtension); } // } // runtimeEnableFilesystem { @Test void test_disable_enable_extension_from_filesystem() throws Exception { hivemqExtensionFromFilesystem.disableExtension("Modifier Extension", "modifier-extension"); hivemqExtensionFromFilesystem.enableExtension("Modifier Extension", "modifier-extension"); } // } } ================================================ FILE: modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoExtensionTestsIT.java ================================================ package org.testcontainers.hivemq.docs; import com.hivemq.client.mqtt.mqtt5.Mqtt5BlockingClient; import com.hivemq.client.mqtt.mqtt5.Mqtt5Client; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.testcontainers.hivemq.HiveMQContainer; import org.testcontainers.hivemq.HiveMQExtension; import org.testcontainers.hivemq.util.MyExtensionWithSubclasses; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.util.concurrent.TimeUnit; @Testcontainers class DemoExtensionTestsIT { // waitStrategy { @Container final HiveMQContainer hivemqWithWaitStrategy = new HiveMQContainer( DockerImageName.parse("hivemq/hivemq4").withTag("4.7.4") ) .withExtension(MountableFile.forClasspathResource("/modifier-extension")) .waitForExtension("Modifier Extension"); // } // extensionClasspath { final HiveMQExtension hiveMQEClasspathxtension = HiveMQExtension .builder() .id("extension-1") .name("my-extension") .version("1.0") .mainClass(MyExtensionWithSubclasses.class) .build(); @Container final HiveMQContainer hivemqWithClasspathExtension = new HiveMQContainer( DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3") ) .waitForExtension(hiveMQEClasspathxtension) .withExtension(hiveMQEClasspathxtension) .withHiveMQConfig(MountableFile.forClasspathResource("/inMemoryConfig.xml")); // } @Test @Timeout(value = 3, unit = TimeUnit.MINUTES) void test() throws Exception { // mqtt5client { final Mqtt5BlockingClient client = Mqtt5Client .builder() .serverPort(hivemqWithClasspathExtension.getMqttPort()) .serverHost(hivemqWithClasspathExtension.getHost()) .buildBlocking(); client.connect(); client.disconnect(); // } } } ================================================ FILE: modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoFilesIT.java ================================================ package org.testcontainers.hivemq.docs; import com.hivemq.client.mqtt.mqtt5.Mqtt5BlockingClient; import com.hivemq.client.mqtt.mqtt5.Mqtt5Client; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.testcontainers.hivemq.HiveMQContainer; import org.testcontainers.hivemq.HiveMQExtension; import org.testcontainers.hivemq.util.MyExtension; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.util.concurrent.TimeUnit; @Testcontainers class DemoFilesIT { // hivemqHome { final HiveMQContainer hivemqFileInHome = new HiveMQContainer( DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3") ) .withFileInHomeFolder( MountableFile.forHostPath("src/test/resources/additionalFile.txt"), "/path/in/home/folder" ); // } // extensionHome { @Container final HiveMQContainer hivemqFileInExtensionHome = new HiveMQContainer( DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3") ) .withExtension( HiveMQExtension .builder() .id("extension-1") .name("my-extension") .version("1.0") .mainClass(MyExtension.class) .build() ) .withFileInExtensionHomeFolder( MountableFile.forHostPath("src/test/resources/additionalFile.txt"), "extension-1", "/path/in/extension/home" ); // } // withLicenses { @Container final HiveMQContainer hivemq = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3")) .withLicense(MountableFile.forHostPath("src/test/resources/myLicense.lic")) .withLicense(MountableFile.forHostPath("src/test/resources/myExtensionLicense.elic")); // } @Test @Timeout(value = 3, unit = TimeUnit.MINUTES) void test() throws Exception { // mqtt5client { final Mqtt5BlockingClient client = Mqtt5Client .builder() .serverPort(hivemq.getMqttPort()) .serverHost(hivemq.getHost()) .buildBlocking(); client.connect(); client.disconnect(); // } } } ================================================ FILE: modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoHiveMQContainerIT.java ================================================ package org.testcontainers.hivemq.docs; import com.hivemq.client.mqtt.mqtt5.Mqtt5BlockingClient; import com.hivemq.client.mqtt.mqtt5.Mqtt5Client; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.slf4j.event.Level; import org.testcontainers.hivemq.HiveMQContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.util.concurrent.TimeUnit; @Testcontainers class DemoHiveMQContainerIT { // ceVersion { @Container final HiveMQContainer hivemqCe = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3")) .withLogLevel(Level.DEBUG); // } // hiveEEVersion { @Container final HiveMQContainer hivemqEe = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq4").withTag("4.7.4")) .withLogLevel(Level.DEBUG); // } // eeVersionWithControlCenter { @Container final HiveMQContainer hivemqEeWithControlCenter = new HiveMQContainer( DockerImageName.parse("hivemq/hivemq4").withTag("4.7.4") ) .withLogLevel(Level.DEBUG) .withHiveMQConfig(MountableFile.forClasspathResource("/inMemoryConfig.xml")) .withControlCenter(); // } // specificVersion { @Container final HiveMQContainer hivemqSpecificVersion = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq-ce:2024.3")); // } @Test @Timeout(value = 3, unit = TimeUnit.MINUTES) void test() throws Exception { // mqtt5client { final Mqtt5BlockingClient client = Mqtt5Client .builder() .serverPort(hivemqCe.getMqttPort()) .serverHost(hivemqCe.getHost()) .buildBlocking(); client.connect(); client.disconnect(); // } } } ================================================ FILE: modules/hivemq/src/test/java/org/testcontainers/hivemq/util/MyExtension.java ================================================ package org.testcontainers.hivemq.util; import com.hivemq.extension.sdk.api.ExtensionMain; import com.hivemq.extension.sdk.api.interceptor.publish.PublishInboundInterceptor; import com.hivemq.extension.sdk.api.parameter.ExtensionStartInput; import com.hivemq.extension.sdk.api.parameter.ExtensionStartOutput; import com.hivemq.extension.sdk.api.parameter.ExtensionStopInput; import com.hivemq.extension.sdk.api.parameter.ExtensionStopOutput; import com.hivemq.extension.sdk.api.services.Services; import com.hivemq.extension.sdk.api.services.intializer.ClientInitializer; import org.jetbrains.annotations.NotNull; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @SuppressWarnings("CodeBlock2Expr") public class MyExtension implements ExtensionMain { @Override public void extensionStart( final @NotNull ExtensionStartInput extensionStartInput, final @NotNull ExtensionStartOutput extensionStartOutput ) { final PublishInboundInterceptor publishInboundInterceptor = (publishInboundInput, publishInboundOutput) -> { publishInboundOutput .getPublishPacket() .setPayload(ByteBuffer.wrap("modified".getBytes(StandardCharsets.UTF_8))); }; final ClientInitializer clientInitializer = (initializerInput, clientContext) -> { clientContext.addPublishInboundInterceptor(publishInboundInterceptor); }; Services.initializerRegistry().setClientInitializer(clientInitializer); } @Override public void extensionStop( final @NotNull ExtensionStopInput extensionStopInput, final @NotNull ExtensionStopOutput extensionStopOutput ) {} } ================================================ FILE: modules/hivemq/src/test/java/org/testcontainers/hivemq/util/MyExtensionWithSubclasses.java ================================================ package org.testcontainers.hivemq.util; import com.hivemq.extension.sdk.api.ExtensionMain; import com.hivemq.extension.sdk.api.parameter.ExtensionStartInput; import com.hivemq.extension.sdk.api.parameter.ExtensionStartOutput; import com.hivemq.extension.sdk.api.parameter.ExtensionStopInput; import com.hivemq.extension.sdk.api.parameter.ExtensionStopOutput; import com.hivemq.extension.sdk.api.services.Services; import com.hivemq.extension.sdk.api.services.intializer.ClientInitializer; import org.jetbrains.annotations.NotNull; public class MyExtensionWithSubclasses implements ExtensionMain { @Override public void extensionStart( final @NotNull ExtensionStartInput extensionStartInput, final @NotNull ExtensionStartOutput extensionStartOutput ) { final ClientInitializer clientInitializer = (initializerInput, clientContext) -> { clientContext.addPublishInboundInterceptor(new PublishModifier()); }; Services.initializerRegistry().setClientInitializer(clientInitializer); } @Override public void extensionStop( final @NotNull ExtensionStopInput extensionStopInput, final @NotNull ExtensionStopOutput extensionStopOutput ) {} } ================================================ FILE: modules/hivemq/src/test/java/org/testcontainers/hivemq/util/PublishModifier.java ================================================ package org.testcontainers.hivemq.util; import com.hivemq.extension.sdk.api.interceptor.publish.PublishInboundInterceptor; import com.hivemq.extension.sdk.api.interceptor.publish.parameter.PublishInboundInput; import com.hivemq.extension.sdk.api.interceptor.publish.parameter.PublishInboundOutput; import org.jetbrains.annotations.NotNull; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; public class PublishModifier implements PublishInboundInterceptor { @Override public void onInboundPublish( final @NotNull PublishInboundInput publishInboundInput, final @NotNull PublishInboundOutput publishInboundOutput ) { publishInboundOutput .getPublishPacket() .setPayload(ByteBuffer.wrap("modified".getBytes(StandardCharsets.UTF_8))); } } ================================================ FILE: modules/hivemq/src/test/java/org/testcontainers/hivemq/util/TestPublishModifiedUtil.java ================================================ package org.testcontainers.hivemq.util; import com.hivemq.client.mqtt.MqttGlobalPublishFilter; import com.hivemq.client.mqtt.mqtt5.Mqtt5BlockingClient; import com.hivemq.client.mqtt.mqtt5.Mqtt5Client; import org.jetbrains.annotations.NotNull; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.concurrent.CompletableFuture; public class TestPublishModifiedUtil { public static void testPublishModified(final int mqttPort, final @NotNull String host) throws Exception { final CompletableFuture publishReceived = new CompletableFuture<>(); final Mqtt5BlockingClient publisher = Mqtt5Client .builder() .serverPort(mqttPort) .serverHost(host) .identifier("publisher") .buildBlocking(); publisher.connect(); final Mqtt5BlockingClient subscriber = Mqtt5Client .builder() .serverPort(mqttPort) .serverHost(host) .identifier("subscriber") .buildBlocking(); subscriber.connect(); subscriber.subscribeWith().topicFilter("test/topic").send(); subscriber .toAsync() .publishes( MqttGlobalPublishFilter.ALL, publish -> { if (Arrays.equals(publish.getPayloadAsBytes(), "modified".getBytes(StandardCharsets.UTF_8))) { publishReceived.complete(null); } else { publishReceived.completeExceptionally( new IllegalArgumentException( "unexpected payload: " + new String(publish.getPayloadAsBytes()) ) ); } } ); publisher.publishWith().topic("test/topic").payload("unmodified".getBytes(StandardCharsets.UTF_8)).send(); try { publishReceived.get(); } finally { publisher.disconnect(); subscriber.disconnect(); } } } ================================================ FILE: modules/hivemq/src/test/resources/additionalFile.txt ================================================ ================================================ FILE: modules/hivemq/src/test/resources/config.xml ================================================ 1883 0.0.0.0 0 ================================================ FILE: modules/hivemq/src/test/resources/inMemoryConfig.xml ================================================ in-memory ================================================ FILE: modules/hivemq/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/hivemq/src/test/resources/modifier-extension/hivemq-extension.xml ================================================ modifier-extension 1.0-SNAPSHOT Modifier Extension HiveMQ GmbH 1000 ================================================ FILE: modules/hivemq/src/test/resources/modifier-extension-wrong-name/hivemq-extension.xml ================================================ modifier-extension 1.0-SNAPSHOT Modifier Extension HiveMQ GmbH 1000 ================================================ FILE: modules/hivemq/src/test/resources/myExtensionLicense.elic ================================================ ================================================ FILE: modules/hivemq/src/test/resources/myLicense.lic ================================================ ================================================ FILE: modules/influxdb/build.gradle ================================================ description = "Testcontainers :: InfluxDB" dependencies { api project(':testcontainers') compileOnly 'org.influxdb:influxdb-java:2.25' testImplementation 'org.influxdb:influxdb-java:2.25' testImplementation "com.influxdb:influxdb-client-java:7.4.0" } ================================================ FILE: modules/influxdb/src/main/java/org/testcontainers/containers/InfluxDBContainer.java ================================================ package org.testcontainers.containers; import lombok.Getter; import org.influxdb.InfluxDB; import org.influxdb.InfluxDBFactory; import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; import org.testcontainers.utility.ComparableVersion; import org.testcontainers.utility.DockerImageName; import java.util.Collections; import java.util.Optional; import java.util.Set; /** * Testcontainers implementation for InfluxDB. *

* Supported image: {@code influxdb} *

* Exposed ports: 8086 */ public class InfluxDBContainer> extends GenericContainer { public static final Integer INFLUXDB_PORT = 8086; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("influxdb"); private static final String DEFAULT_TAG = "1.4.3"; @Deprecated public static final String VERSION = DEFAULT_TAG; private static final int NO_CONTENT_STATUS_CODE = 204; @Getter private String username = "test-user"; @Getter private String password = "test-password"; /** * Properties of InfluxDB 1.x */ private boolean authEnabled = true; private String admin = "admin"; private String adminPassword = "password"; @Getter private String database; /** * Properties of InfluxDB 2.x */ @Getter private String bucket = "test-bucket"; @Getter private String organization = "test-org"; @Getter private Optional retention = Optional.empty(); @Getter private Optional adminToken = Optional.empty(); private final boolean isAtLeastMajorVersion2; /** * @deprecated use {@link #InfluxDBContainer(DockerImageName)} instead */ @Deprecated public InfluxDBContainer() { this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } /** * @deprecated use {@link #InfluxDBContainer(DockerImageName)} instead */ @Deprecated public InfluxDBContainer(final String version) { this(DEFAULT_IMAGE_NAME.withTag(version)); } public InfluxDBContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); this.waitStrategy = new HttpWaitStrategy() .forPath("/ping") .withBasicCredentials(this.username, this.password) .forStatusCode(NO_CONTENT_STATUS_CODE); this.isAtLeastMajorVersion2 = new ComparableVersion(dockerImageName.getVersionPart()).isGreaterThanOrEqualTo("2.0.0"); addExposedPort(INFLUXDB_PORT); } /** * Sets the InfluxDB environment variables based on the version */ @Override protected void configure() { if (this.isAtLeastMajorVersion2) { configureInfluxDBV2(); } else { configureInfluxDBV1(); } } /** * Sets the InfluxDB 2.x environment variables * * @see InfluxDB Dockerhub for full documentation on InfluxDB's * envrinoment variables */ private void configureInfluxDBV2() { addEnv("DOCKER_INFLUXDB_INIT_MODE", "setup"); addEnv("DOCKER_INFLUXDB_INIT_USERNAME", this.username); addEnv("DOCKER_INFLUXDB_INIT_PASSWORD", this.password); addEnv("DOCKER_INFLUXDB_INIT_ORG", this.organization); addEnv("DOCKER_INFLUXDB_INIT_BUCKET", this.bucket); this.retention.ifPresent(ret -> addEnv("DOCKER_INFLUXDB_INIT_RETENTION", ret)); this.adminToken.ifPresent(token -> addEnv("DOCKER_INFLUXDB_INIT_ADMIN_TOKEN", token)); } /** * Sets the InfluxDB 1.x environment variables */ private void configureInfluxDBV1() { addEnv("INFLUXDB_USER", this.username); addEnv("INFLUXDB_USER_PASSWORD", this.password); addEnv("INFLUXDB_HTTP_AUTH_ENABLED", String.valueOf(this.authEnabled)); addEnv("INFLUXDB_ADMIN_USER", this.admin); addEnv("INFLUXDB_ADMIN_PASSWORD", this.adminPassword); addEnv("INFLUXDB_DB", this.database); } @Override public Set getLivenessCheckPortNumbers() { return Collections.singleton(getMappedPort(INFLUXDB_PORT)); } /** * Set user for InfluxDB * * @param username The username to set for the system's initial super-user * @return a reference to this container instance */ public InfluxDBContainer withUsername(final String username) { this.username = username; return this; } /** * Set password for InfluxDB * * @param password The password to set for the system's initial super-user * @return a reference to this container instance */ public InfluxDBContainer withPassword(final String password) { this.password = password; return this; } /** * Determines if authentication should be enabled or not * * @param authEnabled Enables authentication. * @return a reference to this container instance */ public InfluxDBContainer withAuthEnabled(final boolean authEnabled) { this.authEnabled = authEnabled; return this.self(); } /** * Sets the admin user * * @param admin The name of the admin user to be created. If this is unset, no admin user is created. * @return a reference to this container instance */ public InfluxDBContainer withAdmin(final String admin) { this.admin = admin; return this.self(); } /** * Sets the admin password * * @param adminPassword The password for the admin user. If this is unset, a random password is generated and * printed to standard out. * @return a reference to this container instance */ public InfluxDBContainer withAdminPassword(final String adminPassword) { this.adminPassword = adminPassword; return this.self(); } /** * Initializes database with given name * * @param database name of the database. * @return a reference to this container instance */ public InfluxDBContainer withDatabase(final String database) { this.database = database; return this.self(); } /** * Sets the organization name * * @param organization The organization for the initial setup of influxDB. * @return a reference to this container instance */ public InfluxDBContainer withOrganization(final String organization) { this.organization = organization; return this; } /** * Initializes bucket with given name * * @param bucket name of the bucket. * @return a reference to this container instance */ public InfluxDBContainer withBucket(final String bucket) { this.bucket = bucket; return this; } /** * Sets the retention in days * * @param retention days bucket will retain data (0 is infinite, default is 0). * @return a reference to this container instance */ public InfluxDBContainer withRetention(final String retention) { this.retention = Optional.of(retention); return this; } /** * Sets the admin token * * @param adminToken Authentication token to associate with the admin user. * @return a reference to this container instance */ public InfluxDBContainer withAdminToken(final String adminToken) { this.adminToken = Optional.of(adminToken); return this; } /** * @return a url to InfluxDB */ public String getUrl() { return "http://" + getHost() + ":" + getMappedPort(INFLUXDB_PORT); } /** * @return a InfluxDB client for InfluxDB 1.x. * @deprecated Use the new InfluxDB client library. */ @Deprecated public InfluxDB getNewInfluxDB() { final InfluxDB influxDB = InfluxDBFactory.connect(getUrl(), this.username, this.password); influxDB.setDatabase(this.database); return influxDB; } } ================================================ FILE: modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerTest.java ================================================ package org.testcontainers.containers; import com.influxdb.client.InfluxDBClient; import com.influxdb.client.InfluxDBClientFactory; import com.influxdb.client.InfluxDBClientOptions; import com.influxdb.client.QueryApi; import com.influxdb.client.WriteApi; import com.influxdb.client.domain.Bucket; import com.influxdb.client.domain.BucketRetentionRules; import com.influxdb.client.domain.WritePrecision; import com.influxdb.client.write.Point; import com.influxdb.query.FluxRecord; import com.influxdb.query.FluxTable; import org.junit.jupiter.api.Test; import org.testcontainers.utility.DockerImageName; import java.time.Instant; import java.util.List; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; class InfluxDBContainerTest { private static final String USERNAME = "new-test-user"; private static final String PASSWORD = "new-test-password"; private static final String ORG = "new-test-org"; private static final String BUCKET = "new-test-bucket"; private static final String RETENTION = "1w"; private static final String ADMIN_TOKEN = "super-secret-token"; private static final int SECONDS_IN_WEEK = 604800; @Test void getInfluxDBClient() { try ( // constructorWithDefaultVariables { final InfluxDBContainer influxDBContainer = new InfluxDBContainer<>( DockerImageName.parse("influxdb:2.0.7") ) // } ) { influxDBContainer.start(); try (final InfluxDBClient influxDBClient = createClient(influxDBContainer)) { assertThat(influxDBClient).isNotNull(); assertThat(influxDBClient.ping()).isTrue(); } } } @Test void getInfluxDBClientWithAdminToken() { try ( // constructorWithAdminToken { final InfluxDBContainer influxDBContainer = new InfluxDBContainer<>( DockerImageName.parse("influxdb:2.0.7") ) .withAdminToken(ADMIN_TOKEN) // } ) { influxDBContainer.start(); final Optional adminToken = influxDBContainer.getAdminToken(); assertThat(adminToken).isNotEmpty(); try ( final InfluxDBClient influxDBClient = createClientWithToken( influxDBContainer.getUrl(), adminToken.get() ) ) { assertThat(influxDBClient).isNotNull(); assertThat(influxDBClient.ping()).isTrue(); } } } @Test void getBucket() { try ( // constructorWithCustomVariables { final InfluxDBContainer influxDBContainer = new InfluxDBContainer<>( DockerImageName.parse("influxdb:2.0.7") ) .withUsername(USERNAME) .withPassword(PASSWORD) .withOrganization(ORG) .withBucket(BUCKET) .withRetention(RETENTION); // } ) { influxDBContainer.start(); try (final InfluxDBClient influxDBClient = createClient(influxDBContainer)) { final Bucket bucket = influxDBClient.getBucketsApi().findBucketByName(BUCKET); assertThat(bucket).isNotNull(); assertThat(bucket.getName()).isEqualTo(BUCKET); assertThat(bucket.getRetentionRules()) .hasSize(1) .first() .extracting(BucketRetentionRules::getEverySeconds) .isEqualTo(SECONDS_IN_WEEK); } } } @Test void queryForWriteAndRead() { try ( final InfluxDBContainer influxDBContainer = new InfluxDBContainer<>( InfluxDBTestUtils.INFLUXDB_V2_TEST_IMAGE ) .withUsername(USERNAME) .withPassword(PASSWORD) .withOrganization(ORG) .withBucket(BUCKET) .withRetention(RETENTION) ) { influxDBContainer.start(); try (final InfluxDBClient influxDBClient = createClient(influxDBContainer)) { try (final WriteApi writeApi = influxDBClient.makeWriteApi()) { final Point point = Point .measurement("temperature") .addTag("location", "west") .addField("value", 55.0D) .time(Instant.now().toEpochMilli(), WritePrecision.MS); writeApi.writePoint(point); } final String flux = String.format("from(bucket:\"%s\") |> range(start: 0)", BUCKET); final QueryApi queryApi = influxDBClient.getQueryApi(); final FluxTable fluxTable = queryApi.query(flux).get(0); final List records = fluxTable.getRecords(); assertThat(records).hasSize(1); } } } // createInfluxDBClient { public static InfluxDBClient createClient(final InfluxDBContainer influxDBContainer) { final InfluxDBClientOptions influxDBClientOptions = InfluxDBClientOptions .builder() .url(influxDBContainer.getUrl()) .authenticate(influxDBContainer.getUsername(), influxDBContainer.getPassword().toCharArray()) .bucket(influxDBContainer.getBucket()) .org(influxDBContainer.getOrganization()) .build(); return InfluxDBClientFactory.create(influxDBClientOptions); } // } public static InfluxDBClient createClientWithToken(final String url, final String token) { return InfluxDBClientFactory.create(url, token.toCharArray()); } } ================================================ FILE: modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerV1Test.java ================================================ package org.testcontainers.containers; import org.influxdb.InfluxDB; import org.influxdb.InfluxDBFactory; import org.influxdb.dto.Point; import org.influxdb.dto.Query; import org.influxdb.dto.QueryResult; import org.junit.jupiter.api.Test; import org.testcontainers.utility.DockerImageName; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; class InfluxDBContainerV1Test { private static final String TEST_VERSION = InfluxDBTestUtils.INFLUXDB_V1_TEST_IMAGE.getVersionPart(); private static final String DATABASE = "test"; private static final String USER = "new-test-user"; private static final String PASSWORD = "new-test-password"; @Test void createInfluxDBOnlyWithUrlAndCorrectVersion() { try ( // constructorWithDefaultVariables { final InfluxDBContainer influxDBContainer = new InfluxDBContainer<>( DockerImageName.parse("influxdb:1.4.3") ) // } ) { // Start the container. This step might take some time... influxDBContainer.start(); try (final InfluxDB influxDBClient = createInfluxDBWithUrl(influxDBContainer)) { assertThat(influxDBClient).isNotNull(); assertThat(influxDBClient.ping().isGood()).isTrue(); assertThat(influxDBClient.version()).isEqualTo(TEST_VERSION); } } } @Test void getNewInfluxDBWithCorrectVersion() { try ( final InfluxDBContainer influxDBContainer = new InfluxDBContainer<>( InfluxDBTestUtils.INFLUXDB_V1_TEST_IMAGE ) ) { // Start the container. This step might take some time... influxDBContainer.start(); try (final InfluxDB influxDBClient = createInfluxDBWithUrl(influxDBContainer)) { assertThat(influxDBClient).isNotNull(); assertThat(influxDBClient.ping().isGood()).isTrue(); assertThat(influxDBClient.version()).isEqualTo(TEST_VERSION); } } } @Test void describeDatabases() { try ( // constructorWithUserPassword { final InfluxDBContainer influxDBContainer = new InfluxDBContainer<>( DockerImageName.parse("influxdb:1.4.3") ) .withDatabase(DATABASE) .withUsername(USER) .withPassword(PASSWORD) // } ) { // Start the container. This step might take some time... influxDBContainer.start(); try (final InfluxDB influxDBClient = createInfluxDBWithUrl(influxDBContainer)) { assertThat(influxDBClient.describeDatabases()).contains(DATABASE); } } } @Test void queryForWriteAndRead() { try ( final InfluxDBContainer influxDBContainer = new InfluxDBContainer<>( InfluxDBTestUtils.INFLUXDB_V1_TEST_IMAGE ) .withDatabase(DATABASE) .withUsername(USER) .withPassword(PASSWORD) ) { // Start the container. This step might take some time... influxDBContainer.start(); try (final InfluxDB influxDBClient = createInfluxDBWithUrl(influxDBContainer)) { final Point point = Point .measurement("cpu") .time(System.currentTimeMillis(), TimeUnit.MILLISECONDS) .addField("idle", 90L) .addField("user", 9L) .addField("system", 1L) .build(); influxDBClient.write(point); final Query query = new Query("SELECT idle FROM cpu", DATABASE); final QueryResult actual = influxDBClient.query(query); assertThat(actual).isNotNull(); assertThat(actual.getError()).isNull(); assertThat(actual.getResults()).isNotNull(); assertThat(actual.getResults()).hasSize(1); } } } // createInfluxDBClient { public static InfluxDB createInfluxDBWithUrl(final InfluxDBContainer container) { InfluxDB influxDB = InfluxDBFactory.connect( container.getUrl(), container.getUsername(), container.getPassword() ); influxDB.setDatabase(container.getDatabase()); return influxDB; } // } } ================================================ FILE: modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBTestUtils.java ================================================ package org.testcontainers.containers; import org.testcontainers.utility.DockerImageName; public final class InfluxDBTestUtils { static final DockerImageName INFLUXDB_V1_TEST_IMAGE = DockerImageName.parse("influxdb:1.4.3"); static final DockerImageName INFLUXDB_V2_TEST_IMAGE = DockerImageName.parse("influxdb:2.0.7"); } ================================================ FILE: modules/influxdb/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/jdbc/build.gradle ================================================ description = "Testcontainers :: JDBC" dependencies { api project(':testcontainers-database-commons') compileOnly 'org.jetbrains:annotations:26.0.2-1' testImplementation 'commons-dbutils:commons-dbutils:1.8.1' testImplementation 'org.vibur:vibur-dbcp:26.0' testImplementation 'org.apache.tomcat:tomcat-jdbc:11.0.14' testImplementation 'com.zaxxer:HikariCP-java6:2.3.13' testImplementation ('org.mockito:mockito-core:4.11.0') { exclude(module: 'hamcrest-core') } } ================================================ FILE: modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java ================================================ package org.testcontainers.containers; import com.github.dockerjava.api.command.InspectContainerResponse; import lombok.NonNull; import lombok.SneakyThrows; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.testcontainers.containers.traits.LinkableContainer; import org.testcontainers.delegate.DatabaseDelegate; import org.testcontainers.ext.ScriptUtils; import org.testcontainers.jdbc.JdbcDatabaseDelegate; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.sql.Connection; import java.sql.Driver; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Properties; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** * Base class for containers that expose a JDBC connection */ public abstract class JdbcDatabaseContainer> extends GenericContainer implements LinkableContainer { private static final Object DRIVER_LOAD_MUTEX = new Object(); private Driver driver; private List initScriptPaths = new ArrayList<>(); protected Map parameters = new HashMap<>(); protected Map urlParameters = new HashMap<>(); private int startupTimeoutSeconds = 120; private int connectTimeoutSeconds = 120; private static final String QUERY_PARAM_SEPARATOR = "&"; /** * @deprecated use {@link #JdbcDatabaseContainer(DockerImageName)} instead */ public JdbcDatabaseContainer(@NonNull final String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public JdbcDatabaseContainer(@NonNull final Future image) { super(image); } public JdbcDatabaseContainer(final DockerImageName dockerImageName) { super(dockerImageName); } /** * @return the name of the actual JDBC driver to use */ public abstract String getDriverClassName(); /** * @return a JDBC URL that may be used to connect to the dockerized DB */ public abstract String getJdbcUrl(); /** * @return the database name */ public String getDatabaseName() { throw new UnsupportedOperationException(); } /** * @return the standard database username that should be used for connections */ public abstract String getUsername(); /** * @return the standard password that should be used for connections */ public abstract String getPassword(); /** * @return a test query string suitable for testing that this particular database type is alive */ protected abstract String getTestQueryString(); public SELF withUsername(String username) { throw new UnsupportedOperationException(); } public SELF withPassword(String password) { throw new UnsupportedOperationException(); } public SELF withDatabaseName(String dbName) { throw new UnsupportedOperationException(); } public SELF withUrlParam(String paramName, String paramValue) { urlParameters.put(paramName, paramValue); return self(); } /** * Set startup time to allow, including image pull time, in seconds. * * @param startupTimeoutSeconds startup time to allow, including image pull time, in seconds * @return self */ public SELF withStartupTimeoutSeconds(int startupTimeoutSeconds) { this.startupTimeoutSeconds = startupTimeoutSeconds; return self(); } /** * Set time to allow for the database to start and establish an initial connection, in seconds. * * @param connectTimeoutSeconds time to allow for the database to start and establish an initial connection in seconds * @return self */ public SELF withConnectTimeoutSeconds(int connectTimeoutSeconds) { this.connectTimeoutSeconds = connectTimeoutSeconds; return self(); } /** * Sets a script for initialization. * * @param initScriptPath path to the script file * @return self */ public SELF withInitScript(String initScriptPath) { this.initScriptPaths = new ArrayList<>(); this.initScriptPaths.add(initScriptPath); return self(); } /** * Sets an ordered array of scripts for initialization. * * @param initScriptPaths paths to the script files * @return self */ public SELF withInitScripts(String... initScriptPaths) { return withInitScripts(Arrays.asList(initScriptPaths)); } /** * Sets an ordered collection of scripts for initialization. * * @param initScriptPaths paths to the script files * @return self */ public SELF withInitScripts(Iterable initScriptPaths) { this.initScriptPaths = new ArrayList<>(); initScriptPaths.forEach(this.initScriptPaths::add); return self(); } @SneakyThrows(InterruptedException.class) @Override protected void waitUntilContainerStarted() { logger() .info( "Waiting for database connection to become available at {} using query '{}'", getJdbcUrl(), getTestQueryString() ); // Repeatedly try and open a connection to the DB and execute a test query long start = System.nanoTime(); Exception lastConnectionException = null; while ((System.nanoTime() - start) < TimeUnit.SECONDS.toNanos(startupTimeoutSeconds)) { if (!isRunning()) { Thread.sleep(100L); } else { try (Connection connection = createConnection(""); Statement statement = connection.createStatement()) { boolean testQuerySucceeded = statement.execute(this.getTestQueryString()); if (testQuerySucceeded) { return; } } catch (NoDriverFoundException e) { // we explicitly want this exception to fail fast without retries throw e; } catch (Exception e) { lastConnectionException = e; // ignore so that we can try again logger().debug("Failure when trying test query", e); Thread.sleep(100L); } } } throw new IllegalStateException( String.format( "Container is started, but cannot be accessed by (JDBC URL: %s), please check container logs", this.getJdbcUrl() ), lastConnectionException ); } @Override protected void containerIsStarted(InspectContainerResponse containerInfo) { logger().info("Container is started (JDBC URL: {})", this.getJdbcUrl()); runInitScriptIfRequired(); } /** * Obtain an instance of the correct JDBC driver for this particular database container type * * @return a JDBC Driver */ public Driver getJdbcDriverInstance() throws NoDriverFoundException { synchronized (DRIVER_LOAD_MUTEX) { if (driver == null) { try { driver = (Driver) Class.forName(this.getDriverClassName()).newInstance(); } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) { throw new NoDriverFoundException("Could not get Driver", e); } } } return driver; } /** * Creates a connection to the underlying containerized database instance. * * @param queryString query string parameters that should be appended to the JDBC connection URL. * The '?' character must be included * @return a Connection * @throws SQLException if there is a repeated failure to create the connection */ public Connection createConnection(String queryString) throws SQLException, NoDriverFoundException { return createConnection(queryString, new Properties()); } /** * Creates a connection to the underlying containerized database instance. * * @param queryString query string parameters that should be appended to the JDBC connection URL. * The '?' character must be included * @param info additional properties to be passed to the JDBC driver * @return a Connection * @throws SQLException if there is a repeated failure to create the connection */ public Connection createConnection(String queryString, Properties info) throws SQLException, NoDriverFoundException { Properties properties = new Properties(info); properties.put("user", this.getUsername()); properties.put("password", this.getPassword()); final String url = constructUrlForConnection(queryString); final Driver jdbcDriverInstance = getJdbcDriverInstance(); SQLException lastException = null; try { long start = System.nanoTime(); // give up if we hit the time limit or the container stops running for some reason while ((System.nanoTime() - start < TimeUnit.SECONDS.toNanos(connectTimeoutSeconds)) && isRunning()) { try { logger() .debug( "Trying to create JDBC connection using {} to {} with properties: {}", jdbcDriverInstance.getClass().getName(), url, properties ); return jdbcDriverInstance.connect(url, properties); } catch (SQLException e) { lastException = e; Thread.sleep(100L); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } throw new SQLException("Could not create new connection", lastException); } /** * Template method for constructing the JDBC URL to be used for creating {@link Connection}s. * This should be overridden if the JDBC URL and query string concatenation or URL string * construction needs to be different to normal. * * @param queryString query string parameters that should be appended to the JDBC connection URL. * The '?' character must be included * @return a full JDBC URL including queryString */ protected String constructUrlForConnection(String queryString) { String baseUrl = getJdbcUrl(); if ("".equals(queryString)) { return baseUrl; } if (!queryString.startsWith("?")) { throw new IllegalArgumentException("The '?' character must be included"); } return baseUrl.contains("?") ? baseUrl + QUERY_PARAM_SEPARATOR + queryString.substring(1) : baseUrl + queryString; } protected String constructUrlParameters(String startCharacter, String delimiter) { return constructUrlParameters(startCharacter, delimiter, StringUtils.EMPTY); } protected String constructUrlParameters(String startCharacter, String delimiter, String endCharacter) { String urlParameters = ""; if (!this.urlParameters.isEmpty()) { String additionalParameters = this.urlParameters.entrySet().stream().map(Object::toString).collect(Collectors.joining(delimiter)); urlParameters = startCharacter + additionalParameters + endCharacter; } return urlParameters; } @Deprecated protected void optionallyMapResourceParameterAsVolume( @NotNull String paramName, @NotNull String pathNameInContainer, @NotNull String defaultResource ) { optionallyMapResourceParameterAsVolume(paramName, pathNameInContainer, defaultResource, null); } protected void optionallyMapResourceParameterAsVolume( @NotNull String paramName, @NotNull String pathNameInContainer, @NotNull String defaultResource, @Nullable Integer fileMode ) { String resourceName = parameters.getOrDefault(paramName, defaultResource); if (resourceName != null) { final MountableFile mountableFile = MountableFile.forClasspathResource(resourceName, fileMode); withCopyFileToContainer(mountableFile, pathNameInContainer); } } /** * Load init script content and apply it to the database if initScriptPath is set */ protected void runInitScriptIfRequired() { initScriptPaths .stream() .filter(Objects::nonNull) .forEach(path -> ScriptUtils.runInitScript(getDatabaseDelegate(), path)); } public void setParameters(Map parameters) { this.parameters = parameters; } @SuppressWarnings("unused") public void addParameter(String paramName, String value) { this.parameters.put(paramName, value); } /** * @return startup time to allow, including image pull time, in seconds * @deprecated should not be overridden anymore, use {@link #withStartupTimeoutSeconds(int)} in constructor instead */ @Deprecated protected int getStartupTimeoutSeconds() { return startupTimeoutSeconds; } /** * @return time to allow for the database to start and establish an initial connection, in seconds * @deprecated should not be overridden anymore, use {@link #withConnectTimeoutSeconds(int)} in constructor instead */ @Deprecated protected int getConnectTimeoutSeconds() { return connectTimeoutSeconds; } protected DatabaseDelegate getDatabaseDelegate() { return new JdbcDatabaseDelegate(this, ""); } public static class NoDriverFoundException extends RuntimeException { public NoDriverFoundException(String message, Throwable e) { super(message, e); } } } ================================================ FILE: modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainerProvider.java ================================================ package org.testcontainers.containers; import lombok.extern.slf4j.Slf4j; import org.testcontainers.jdbc.ConnectionUrl; import java.util.Objects; /** * Base class for classes that can provide a JDBC container. */ @Slf4j public abstract class JdbcDatabaseContainerProvider { /** * Tests if the specified database type is supported by this Container Provider. It should match to the base image name. * @param databaseType {@link String} * @return true when provider can handle this database type, else false. */ public abstract boolean supports(String databaseType); /** * Instantiate a new {@link JdbcDatabaseContainer} without any specified image tag. Subclasses should * override this method if possible, to provide a default tag that is more stable than latest`. * * @return Instance of {@link JdbcDatabaseContainer} */ public JdbcDatabaseContainer newInstance() { log.warn( "No explicit version tag was provided in JDBC URL and this class ({}) does not " + "override newInstance() to set a default tag. `latest` will be used but results may " + "be unreliable!", this.getClass().getCanonicalName() ); return this.newInstance("latest"); } /** * Instantiate a new {@link JdbcDatabaseContainer} with specified image tag. * @param tag * @return Instance of {@link JdbcDatabaseContainer} */ public abstract JdbcDatabaseContainer newInstance(String tag); /** * Instantiate a new {@link JdbcDatabaseContainer} using information provided with {@link ConnectionUrl}. * @param url {@link ConnectionUrl} * @return Instance of {@link JdbcDatabaseContainer} */ public JdbcDatabaseContainer newInstance(ConnectionUrl url) { final JdbcDatabaseContainer result; if (url.getImageTag().isPresent()) { result = newInstance(url.getImageTag().get()); } else { result = newInstance(); } result.withReuse(url.isReusable()); return result; } protected JdbcDatabaseContainer newInstanceFromConnectionUrl( ConnectionUrl connectionUrl, final String userParamName, final String pwdParamName ) { Objects.requireNonNull(connectionUrl, "Connection URL cannot be null"); final String databaseName = connectionUrl.getDatabaseName().orElse("test"); final String user = connectionUrl.getQueryParameters().getOrDefault(userParamName, "test"); final String password = connectionUrl.getQueryParameters().getOrDefault(pwdParamName, "test"); final JdbcDatabaseContainer instance; if (connectionUrl.getImageTag().isPresent()) { instance = newInstance(connectionUrl.getImageTag().get()); } else { instance = newInstance(); } return instance .withReuse(connectionUrl.isReusable()) .withDatabaseName(databaseName) .withUsername(user) .withPassword(password); } } ================================================ FILE: modules/jdbc/src/main/java/org/testcontainers/jdbc/ConnectionDelegate.java ================================================ package org.testcontainers.jdbc; import lombok.RequiredArgsConstructor; import lombok.experimental.Delegate; import java.sql.Connection; @RequiredArgsConstructor class ConnectionDelegate implements Connection { @Delegate private final Connection delegate; } ================================================ FILE: modules/jdbc/src/main/java/org/testcontainers/jdbc/ConnectionUrl.java ================================================ package org.testcontainers.jdbc; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import org.testcontainers.UnstableAPI; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; /** * This is an Immutable class holding JDBC Connection Url and its parsed components, used by {@link ContainerDatabaseDriver}. *

* {@link ConnectionUrl#parseUrl()} method must be called after instantiating this class. */ @EqualsAndHashCode(of = "url") @Getter public class ConnectionUrl { private String url; private String databaseType; private Optional imageTag; /** * This is a part of the connection string that may specify host:port/databasename. * It may vary for different clients and so clients can parse it as needed. */ private String dbHostString; private boolean inDaemonMode = false; private Optional databaseHost = Optional.empty(); private Optional databasePort = Optional.empty(); private Optional databaseName = Optional.empty(); private Optional initScriptPath = Optional.empty(); @UnstableAPI private boolean reusable = false; private Optional initFunction = Optional.empty(); private Optional queryString; private Map containerParameters; private Map queryParameters; private Map tmpfsOptions = new HashMap<>(); public static ConnectionUrl newInstance(final String url) { ConnectionUrl connectionUrl = new ConnectionUrl(url); connectionUrl.parseUrl(); return connectionUrl; } private ConnectionUrl(final String url) { this.url = Objects.requireNonNull(url, "Connection URL cannot be null"); } public static boolean accepts(final String url) { return url.startsWith("jdbc:tc:"); } /** * This method applies various REGEX Patterns to parse the URL associated with this instance. * This is called from a @{@link ConnectionUrl#newInstance(String)} static factory method to create immutable instance of * {@link ConnectionUrl}. * To avoid mutation after class is instantiated, this method should not be publicly accessible. */ private void parseUrl() { /* Extract from the JDBC connection URL: * The database type (e.g. mysql, postgresql, ...) * The docker tag, if provided. * The URL query string, if provided */ Matcher urlMatcher = Patterns.URL_MATCHING_PATTERN.matcher(this.getUrl()); if (!urlMatcher.matches()) { //Try for Oracle pattern urlMatcher = Patterns.ORACLE_URL_MATCHING_PATTERN.matcher(this.getUrl()); if (!urlMatcher.matches()) { throw new IllegalArgumentException( "JDBC URL matches jdbc:tc: prefix but the database or tag name could not be identified" ); } } databaseType = urlMatcher.group("databaseType"); imageTag = Optional.ofNullable(urlMatcher.group("imageTag")); //String like hostname:port/database name, which may vary based on target database. //Clients can further parse it as needed. dbHostString = urlMatcher.group("dbHostString"); //In case it matches to the default pattern Matcher dbInstanceMatcher = Patterns.DB_INSTANCE_MATCHING_PATTERN.matcher(dbHostString); if (dbInstanceMatcher.matches()) { databaseHost = Optional.ofNullable(dbInstanceMatcher.group("databaseHost")); databasePort = Optional.ofNullable(dbInstanceMatcher.group("databasePort")).map(Integer::valueOf); databaseName = Optional.of(dbInstanceMatcher.group("databaseName")); } queryParameters = Collections.unmodifiableMap( parseQueryParameters(Optional.ofNullable(urlMatcher.group("queryParameters")).orElse("")) ); String query = queryParameters .entrySet() .stream() .map(e -> e.getKey() + "=" + e.getValue()) .collect(Collectors.joining("&")); if (query.trim().length() == 0) { queryString = Optional.empty(); } else { queryString = Optional.of("?" + query); } containerParameters = Collections.unmodifiableMap(parseContainerParameters()); tmpfsOptions = parseTmpfsOptions(containerParameters); initScriptPath = Optional.ofNullable(containerParameters.get("TC_INITSCRIPT")); reusable = Boolean.parseBoolean(containerParameters.get("TC_REUSABLE")); Matcher funcMatcher = Patterns.INITFUNCTION_MATCHING_PATTERN.matcher(this.getUrl()); if (funcMatcher.matches()) { initFunction = Optional.of(new InitFunctionDef(funcMatcher.group(2), funcMatcher.group(4))); } Matcher daemonMatcher = Patterns.DAEMON_MATCHING_PATTERN.matcher(this.getUrl()); inDaemonMode = daemonMatcher.matches() && Boolean.parseBoolean(daemonMatcher.group(2)); } private Map parseTmpfsOptions(Map containerParameters) { if (!containerParameters.containsKey("TC_TMPFS")) { return Collections.emptyMap(); } String tmpfsOptions = containerParameters.get("TC_TMPFS"); return Stream .of(tmpfsOptions.split(",")) .collect(Collectors.toMap(string -> string.split(":")[0], string -> string.split(":")[1])); } /** * Get the Testcontainers Parameters such as Init Function, Init Script path etc. * * @return {@link Map} */ private Map parseContainerParameters() { Map results = new HashMap<>(); Matcher matcher = Patterns.TC_PARAM_MATCHING_PATTERN.matcher(this.getUrl()); while (matcher.find()) { String key = matcher.group(1); String value = matcher.group(2); results.put(key, value); } return results; } /** * Get all Query parameters specified in the Connection URL after ?. This DOES NOT include Testcontainers (TC_*) parameters. * * @return {@link Map} */ private Map parseQueryParameters(final String queryString) { Map results = new HashMap<>(); Matcher matcher = Patterns.QUERY_PARAM_MATCHING_PATTERN.matcher(queryString); while (matcher.find()) { String key = matcher.group(1); String value = matcher.group(2); if (!key.matches(Patterns.TC_PARAM_NAME_PATTERN)) { results.put(key, value); } } return results; } public Map getTmpfsOptions() { return Collections.unmodifiableMap(tmpfsOptions); } /** * This interface defines the Regex Patterns used by {@link ConnectionUrl}. */ public interface Patterns { Pattern URL_MATCHING_PATTERN = Pattern.compile( "jdbc:tc:" + "(?[a-z0-9]+)" + "(:(?[^:]+))?" + "://" + "(?[^?]+)" + "(?\\?.*)?" ); Pattern ORACLE_URL_MATCHING_PATTERN = Pattern.compile( "jdbc:tc:" + "(?[a-z]+)" + "(:(?(?!thin).+))?:thin:(//)?" + "(" + "(?[^:" + "?^/]+)/(?[^?^/]+)" + ")?" + "@" + "(?[^?]+)" + "(?\\?.*)?" ); //Matches to part of string - hostname:port/databasename Pattern DB_INSTANCE_MATCHING_PATTERN = Pattern.compile( "(?[^:]+)?" + "(:(?[0-9]+))?" + "(" + "(?[:/])" + "|" + ";databaseName=" + ")" + "(?[^\\\\?]+)" ); Pattern DAEMON_MATCHING_PATTERN = Pattern.compile(".*([?&]?)TC_DAEMON=([^?&]+).*"); /** * @deprecated for removal */ @Deprecated Pattern INITSCRIPT_MATCHING_PATTERN = Pattern.compile(".*([?&]?)TC_INITSCRIPT=([^?&]+).*"); Pattern INITFUNCTION_MATCHING_PATTERN = Pattern.compile( ".*([?&]?)TC_INITFUNCTION=" + "((\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*\\.)*\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)" + "::" + "(\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)" + ".*" ); String TC_PARAM_NAME_PATTERN = "(TC_[A-Z_]+)"; Pattern TC_PARAM_MATCHING_PATTERN = Pattern.compile(TC_PARAM_NAME_PATTERN + "=([^?&]+)"); Pattern QUERY_PARAM_MATCHING_PATTERN = Pattern.compile("([^?&=]+)=([^?&]*)"); } @Getter @AllArgsConstructor public class InitFunctionDef { private String className; private String methodName; } } ================================================ FILE: modules/jdbc/src/main/java/org/testcontainers/jdbc/ConnectionWrapper.java ================================================ package org.testcontainers.jdbc; import java.sql.Connection; import java.sql.SQLException; public class ConnectionWrapper extends ConnectionDelegate { private final Runnable closeCallback; public ConnectionWrapper(Connection connection, Runnable runnable) { super(connection); this.closeCallback = runnable; } @Override public void close() throws SQLException { super.close(); try { closeCallback.run(); } catch (Exception e) { e.printStackTrace(); } } } ================================================ FILE: modules/jdbc/src/main/java/org/testcontainers/jdbc/ContainerDatabaseDriver.java ================================================ package org.testcontainers.jdbc; import org.apache.commons.io.IOUtils; import org.slf4j.LoggerFactory; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.containers.JdbcDatabaseContainerProvider; import org.testcontainers.delegate.DatabaseDelegate; import org.testcontainers.ext.ScriptUtils; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; import java.nio.charset.StandardCharsets; import java.sql.Connection; import java.sql.Driver; import java.sql.DriverManager; import java.sql.DriverPropertyInfo; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Properties; import java.util.ServiceLoader; import java.util.Set; import java.util.logging.Logger; import javax.script.ScriptException; /** * Test Containers JDBC proxy driver. This driver will handle JDBC URLs of the form: *

* jdbc:tc:type://host:port/database?querystring *

* where type is a supported database type (e.g. mysql, postgresql, oracle). Behind the scenes a new * docker container will be launched running the required database engine. New JDBC connections will be created * using the database's standard driver implementation, connected to the container. *

* If TC_INITSCRIPT is set in querystring, it will be used as the path for an init script that * should be run to initialize the database after the container is created. This should be a classpath resource. *

* Similarly TC_INITFUNCTION may be a method reference for a function that can initialize the database. * Such a function must accept a javax.sql.Connection as its only parameter. * An example of a valid method reference would be com.myapp.SomeClass::initFunction */ public class ContainerDatabaseDriver implements Driver { private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(ContainerDatabaseDriver.class); private Driver delegate; private static final Map> containerConnections = new HashMap<>(); private static final Map jdbcUrlContainerCache = new HashMap<>(); private static final Set initializedContainers = new HashSet<>(); private static final String FILE_PATH_PREFIX = "file:"; static { load(); } private static void load() { try { DriverManager.registerDriver(new ContainerDatabaseDriver()); } catch (SQLException e) { LOGGER.warn("Failed to register driver", e); } } @Override public boolean acceptsURL(String url) throws SQLException { return url.startsWith("jdbc:tc:"); } @Override public synchronized Connection connect(String url, final Properties info) throws SQLException { /* The driver should return "null" if it realizes it is the wrong kind of driver to connect to the given URL. */ if (!acceptsURL(url)) { return null; } ConnectionUrl connectionUrl = ConnectionUrl.newInstance(url); synchronized (jdbcUrlContainerCache) { String queryString = connectionUrl.getQueryString().orElse(""); /* If we already have a running container for this exact connection string, we want to connect to that rather than create a new container */ JdbcDatabaseContainer container = jdbcUrlContainerCache.get(connectionUrl.getUrl()); if (container == null) { LOGGER.debug("Container not found in cache, creating new instance"); Map parameters = connectionUrl.getContainerParameters(); /* Find a matching container type using ServiceLoader. */ ServiceLoader databaseContainers = ServiceLoader.load( JdbcDatabaseContainerProvider.class ); for (JdbcDatabaseContainerProvider candidateContainerType : databaseContainers) { if (candidateContainerType.supports(connectionUrl.getDatabaseType())) { container = candidateContainerType.newInstance(connectionUrl); container.withTmpFs(connectionUrl.getTmpfsOptions()); delegate = container.getJdbcDriverInstance(); } } if (container == null) { throw new UnsupportedOperationException( "Database name " + connectionUrl.getDatabaseType() + " not supported" ); } /* Cache the container before starting to prevent race conditions when a connection pool is started up */ jdbcUrlContainerCache.put(url, container); /* Pass possible container-specific parameters */ container.setParameters(parameters); /* Start the container */ container.start(); } /* Create a connection using the delegated driver. The container must be ready to accept connections. */ Connection connection = container.createConnection(queryString, info); /* If this container has not been initialized, AND an init script or function has been specified, use it */ if (!initializedContainers.contains(container.getContainerId())) { DatabaseDelegate databaseDelegate = new JdbcDatabaseDelegate(container, queryString); runInitScriptIfRequired(connectionUrl, databaseDelegate); runInitFunctionIfRequired(connectionUrl, connection); initializedContainers.add(container.getContainerId()); } return wrapConnection(connection, container, connectionUrl); } } /** * Wrap the connection, setting up a callback to be called when the connection is closed. *

* When there are no more open connections, the container itself will be stopped. * * @param connection the new connection to be wrapped * @param container the container which the connection is associated with * @param connectionUrl {@link ConnectionUrl} instance representing JDBC Url for this connection * @return the connection, wrapped */ private Connection wrapConnection( final Connection connection, final JdbcDatabaseContainer container, final ConnectionUrl connectionUrl ) { final boolean isDaemon = connectionUrl.isInDaemonMode() || connectionUrl.isReusable(); Set connections = containerConnections.computeIfAbsent( container.getContainerId(), k -> new HashSet<>() ); connections.add(connection); final Set finalConnections = connections; return new ConnectionWrapper( connection, () -> { finalConnections.remove(connection); if (!isDaemon && finalConnections.isEmpty()) { synchronized (jdbcUrlContainerCache) { container.stop(); jdbcUrlContainerCache.remove(connectionUrl.getUrl()); } } } ); } /** * Run an init script from the classpath. * * @param connectionUrl {@link ConnectionUrl} instance representing JDBC Url with init script. * @param databaseDelegate database delegate to apply init scripts to the database * @throws SQLException on script or DB error */ private void runInitScriptIfRequired(final ConnectionUrl connectionUrl, DatabaseDelegate databaseDelegate) throws SQLException { if (connectionUrl.getInitScriptPath().isPresent()) { String initScriptPath = connectionUrl.getInitScriptPath().get(); try { URL resource; if (initScriptPath.startsWith(FILE_PATH_PREFIX)) { //relative workdir path resource = new URL(initScriptPath); } else { //classpath resource resource = Thread.currentThread().getContextClassLoader().getResource(initScriptPath); } if (resource == null) { LOGGER.warn("Could not load classpath init script: {}", initScriptPath); throw new SQLException( "Could not load classpath init script: " + initScriptPath + ". Resource not found." ); } String sql = IOUtils.toString(resource, StandardCharsets.UTF_8); ScriptUtils.executeDatabaseScript(databaseDelegate, initScriptPath, sql); } catch (IOException e) { LOGGER.warn("Could not load classpath init script: {}", initScriptPath); throw new SQLException("Could not load classpath init script: " + initScriptPath, e); } catch (ScriptException e) { LOGGER.error("Error while executing init script: {}", initScriptPath, e); throw new SQLException("Error while executing init script: " + initScriptPath, e); } } } /** * Run an init function (must be a public static method on an accessible class). * * @param connectionUrl {@link ConnectionUrl} instance representing JDBC Url with r init function declarations. * @param connection JDBC connection to apply init functions to. * @throws SQLException on script or DB error */ private void runInitFunctionIfRequired(final ConnectionUrl connectionUrl, Connection connection) throws SQLException { if (connectionUrl.getInitFunction().isPresent()) { String className = connectionUrl.getInitFunction().get().getClassName(); String methodName = connectionUrl.getInitFunction().get().getMethodName(); try { Class initFunctionClazz = Class.forName(className); Method method = initFunctionClazz.getMethod(methodName, Connection.class); method.invoke(null, connection); } catch ( ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e ) { LOGGER.error("Error while executing init function: {}::{}", className, methodName, e); throw new SQLException("Error while executing init function: " + className + "::" + methodName, e); } } } @Override public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException { return delegate != null ? delegate.getPropertyInfo(url, info) : new DriverPropertyInfo[0]; } @Override public int getMajorVersion() { return delegate != null ? delegate.getMajorVersion() : 1; } @Override public int getMinorVersion() { return delegate != null ? delegate.getMinorVersion() : 0; } @Override public boolean jdbcCompliant() { return delegate != null && delegate.jdbcCompliant(); } @Override public Logger getParentLogger() throws SQLFeatureNotSupportedException { if (delegate != null) { return delegate.getParentLogger(); } throw new SQLFeatureNotSupportedException("getParentLogger not supported"); } /** * Utility method to kill ALL database containers directly from test support code. It shouldn't be necessary to use this, * but it is provided for convenience - e.g. for situations where many different database containers are being * tested and cleanup is needed to limit resource usage. */ public static void killContainers() { synchronized (jdbcUrlContainerCache) { jdbcUrlContainerCache.values().forEach(JdbcDatabaseContainer::stop); jdbcUrlContainerCache.clear(); containerConnections.clear(); initializedContainers.clear(); } } /** * Utility method to kill a database container directly from test support code. It shouldn't be necessary to use this, * but it is provided for convenience - e.g. for situations where many different database containers are being * tested and cleanup is needed to limit resource usage. * * @param jdbcUrl the JDBC URL of the container which should be killed */ public static void killContainer(String jdbcUrl) { synchronized (jdbcUrlContainerCache) { JdbcDatabaseContainer container = jdbcUrlContainerCache.get(jdbcUrl); if (container != null) { container.stop(); jdbcUrlContainerCache.remove(jdbcUrl); containerConnections.remove(container.getContainerId()); initializedContainers.remove(container.getContainerId()); } } } /** * Utility method to get an instance of a database container given its JDBC URL. * * @param jdbcUrl the JDBC URL of the container instance to get * @return an instance of database container or null if no container associated with JDBC URL */ static JdbcDatabaseContainer getContainer(String jdbcUrl) { synchronized (jdbcUrlContainerCache) { return jdbcUrlContainerCache.get(jdbcUrl); } } } ================================================ FILE: modules/jdbc/src/main/java/org/testcontainers/jdbc/ContainerLessJdbcDelegate.java ================================================ package org.testcontainers.jdbc; import lombok.extern.slf4j.Slf4j; import org.testcontainers.exception.ConnectionCreationException; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; /** * Containerless jdbc database delegate * * Is used only with deprecated ScriptUtils * * @see org.testcontainers.ext.ScriptUtils */ @Slf4j public class ContainerLessJdbcDelegate extends JdbcDatabaseDelegate { private Connection connection; public ContainerLessJdbcDelegate(Connection connection) { super(null, ""); this.connection = connection; } @Override protected Statement createNewConnection() { try { return connection.createStatement(); } catch (SQLException e) { log.error("Could create JDBC statement"); throw new ConnectionCreationException("Could create JDBC statement", e); } } } ================================================ FILE: modules/jdbc/src/main/java/org/testcontainers/jdbc/JdbcDatabaseDelegate.java ================================================ package org.testcontainers.jdbc; import lombok.extern.slf4j.Slf4j; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.delegate.AbstractDatabaseDelegate; import org.testcontainers.exception.ConnectionCreationException; import org.testcontainers.ext.ScriptUtils; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; /** * JDBC database delegate */ @Slf4j public class JdbcDatabaseDelegate extends AbstractDatabaseDelegate { private JdbcDatabaseContainer container; private Connection connection; private String queryString; public JdbcDatabaseDelegate(JdbcDatabaseContainer container, String queryString) { this.container = container; this.queryString = queryString; } @Override protected Statement createNewConnection() { try { connection = container.createConnection(queryString); return connection.createStatement(); } catch (SQLException e) { log.error("Could not obtain JDBC connection"); throw new ConnectionCreationException("Could not obtain JDBC connection", e); } } @Override public void execute( String statement, String scriptPath, int lineNumber, boolean continueOnError, boolean ignoreFailedDrops ) { try { boolean rowsAffected = getConnection().execute(statement); log.debug("{} returned as updateCount for SQL: {}", rowsAffected, statement); } catch (SQLException ex) { boolean dropStatement = statement.trim().toLowerCase().startsWith("drop"); if (continueOnError || (dropStatement && ignoreFailedDrops)) { log.debug( "Failed to execute SQL script statement at line {} of resource {}: {}", lineNumber, scriptPath, statement, ex ); } else { throw new ScriptUtils.ScriptStatementFailedException(statement, lineNumber, scriptPath, ex); } } } @Override protected void closeConnectionQuietly(Statement statement) { try { statement.close(); connection.close(); } catch (Exception e) { log.error("Could not close JDBC connection", e); } } } ================================================ FILE: modules/jdbc/src/main/java/org/testcontainers/jdbc/ext/ScriptUtils.java ================================================ /* * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.testcontainers.jdbc.ext; import org.testcontainers.jdbc.ContainerLessJdbcDelegate; import java.sql.Connection; import java.util.List; import javax.script.ScriptException; /** * Wrapper for database-agnostic ScriptUtils * * @see org.testcontainers.ext.ScriptUtils * @deprecated Needed only to keep binary compatibility for this internal API. Consider using database-agnostic ScriptUtils */ public abstract class ScriptUtils { /** * Default statement separator within SQL scripts. */ public static final String DEFAULT_STATEMENT_SEPARATOR = ";"; /** * Fallback statement separator within SQL scripts. *

Used if neither a custom defined separator nor the * {@link #DEFAULT_STATEMENT_SEPARATOR} is present in a given script. */ public static final String FALLBACK_STATEMENT_SEPARATOR = "\n"; /** * Default prefix for line comments within SQL scripts. */ public static final String DEFAULT_COMMENT_PREFIX = "--"; /** * Default start delimiter for block comments within SQL scripts. */ public static final String DEFAULT_BLOCK_COMMENT_START_DELIMITER = "/*"; /** * Default end delimiter for block comments within SQL scripts. */ public static final String DEFAULT_BLOCK_COMMENT_END_DELIMITER = "*/"; /** * Prevent instantiation of this utility class. */ private ScriptUtils() { /* no-op */ } /** * @see org.testcontainers.ext.ScriptUtils * @deprecated Needed only to keep binary compatibility for this internal API. Consider using database-agnostic ScriptUtils */ public static void splitSqlScript( String resource, String script, String separator, String commentPrefix, String blockCommentStartDelimiter, String blockCommentEndDelimiter, List statements ) { org.testcontainers.ext.ScriptUtils.splitSqlScript( resource, script, separator, commentPrefix, blockCommentStartDelimiter, blockCommentEndDelimiter, statements ); } /** * @see org.testcontainers.ext.ScriptUtils * @deprecated Needed only to keep binary compatibility for this internal API. Consider using database-agnostic ScriptUtils */ public static boolean containsSqlScriptDelimiters(String script, String delim) { return org.testcontainers.ext.ScriptUtils.containsSqlScriptDelimiters(script, delim); } /** * @see org.testcontainers.ext.ScriptUtils * @deprecated Needed only to keep binary compatibility for this internal API. Consider using database-agnostic ScriptUtils */ public static void executeSqlScript(Connection connection, String scriptPath, String script) throws ScriptException { org.testcontainers.ext.ScriptUtils.executeDatabaseScript( new ContainerLessJdbcDelegate(connection), scriptPath, script ); } /** * @see org.testcontainers.ext.ScriptUtils * @deprecated Needed only to keep binary compatibility for this internal API. Consider using database-agnostic ScriptUtils */ public static void executeSqlScript( Connection connection, String scriptPath, String script, boolean continueOnError, boolean ignoreFailedDrops, String commentPrefix, String separator, String blockCommentStartDelimiter, String blockCommentEndDelimiter ) throws ScriptException { org.testcontainers.ext.ScriptUtils.executeDatabaseScript( new ContainerLessJdbcDelegate(connection), scriptPath, script, continueOnError, ignoreFailedDrops, commentPrefix, separator, blockCommentStartDelimiter, blockCommentEndDelimiter ); } } ================================================ FILE: modules/jdbc/src/main/resources/META-INF/services/java.sql.Driver ================================================ org.testcontainers.jdbc.ContainerDatabaseDriver ================================================ FILE: modules/jdbc/src/test/java/org/testcontainers/containers/JdbcDatabaseContainerTest.java ================================================ package org.testcontainers.containers; import lombok.NonNull; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import java.sql.Connection; import java.sql.SQLException; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.Mockito.mock; class JdbcDatabaseContainerTest { @Test void anExceptionIsThrownIfJdbcIsNotAvailable() { JdbcDatabaseContainer jdbcContainer = new JdbcDatabaseContainerStub("mysql:latest") .withStartupTimeoutSeconds(1); assertThatExceptionOfType(IllegalStateException.class).isThrownBy(jdbcContainer::waitUntilContainerStarted); } static class JdbcDatabaseContainerStub extends JdbcDatabaseContainer { public JdbcDatabaseContainerStub(@NonNull String dockerImageName) { super(dockerImageName); } @Override public String getDriverClassName() { return null; } @Override public String getJdbcUrl() { return null; } @Override public String getUsername() { return null; } @Override public String getPassword() { return null; } @Override protected String getTestQueryString() { return null; } @Override public boolean isRunning() { return true; } @Override public Connection createConnection(String queryString) throws SQLException, NoDriverFoundException { throw new SQLException("Could not create new connection"); } @Override protected Logger logger() { return mock(Logger.class); } @Override public void setDockerImageName(@NonNull String dockerImageName) {} } } ================================================ FILE: modules/jdbc/src/test/java/org/testcontainers/jdbc/ConnectionUrlDriversTests.java ================================================ package org.testcontainers.jdbc; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import java.util.Optional; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; /** * This Test class validates that all supported JDBC URL's can be parsed by ConnectionUrl class. */ class ConnectionUrlDriversTests { public static Stream data() { return Stream.of( Arguments.arguments( "jdbc:tc:mysql:8.0.36://hostname/test", "mysql", Optional.of("8.0.36"), "hostname/test", "test" ), Arguments.arguments("jdbc:tc:mysql://hostname/test", "mysql", Optional.empty(), "hostname/test", "test"), Arguments.arguments( "jdbc:tc:postgresql:1.2.3://hostname/test", "postgresql", Optional.of("1.2.3"), "hostname/test", "test" ), Arguments.arguments( "jdbc:tc:postgresql://hostname/test", "postgresql", Optional.empty(), "hostname/test", "test" ), Arguments.arguments( "jdbc:tc:sqlserver:1.2.3://localhost;instance=SQLEXPRESS:1433;databaseName=test", "sqlserver", Optional.of("1.2.3"), "localhost;instance=SQLEXPRESS:1433;databaseName=test", "test" ), Arguments.arguments( "jdbc:tc:sqlserver://localhost;instance=SQLEXPRESS:1433;databaseName=test", "sqlserver", Optional.empty(), "localhost;instance=SQLEXPRESS:1433;databaseName=test", "test" ), Arguments.arguments( "jdbc:tc:mariadb:1.2.3://localhost:3306/test", "mariadb", Optional.of("1.2.3"), "localhost:3306/test", "test" ), Arguments.arguments( "jdbc:tc:mariadb://localhost:3306/test", "mariadb", Optional.empty(), "localhost:3306/test", "test" ), Arguments.arguments( "jdbc:tc:oracle:1.2.3:thin://@localhost:1521/test", "oracle", Optional.of("1.2.3"), "localhost:1521/test", "test" ), Arguments.arguments( "jdbc:tc:oracle:1.2.3:thin:@localhost:1521/test", "oracle", Optional.of("1.2.3"), "localhost:1521/test", "test" ), Arguments.arguments( "jdbc:tc:oracle:thin:@localhost:1521/test", "oracle", Optional.empty(), "localhost:1521/test", "test" ), Arguments.arguments( "jdbc:tc:oracle:1.2.3:thin:@localhost:1521:test", "oracle", Optional.of("1.2.3"), "localhost:1521:test", "test" ), Arguments.arguments( "jdbc:tc:oracle:1.2.3:thin://@localhost:1521:test", "oracle", Optional.of("1.2.3"), "localhost:1521:test", "test" ), Arguments.arguments( "jdbc:tc:oracle:1.2.3-anything:thin://@localhost:1521:test", "oracle", Optional.of("1.2.3-anything"), "localhost:1521:test", "test" ), Arguments.arguments( "jdbc:tc:oracle:thin:@localhost:1521:test", "oracle", Optional.empty(), "localhost:1521:test", "test" ) ); } @ParameterizedTest(name = "{index} - {0}") @MethodSource("data") void test(String jdbcUrl, String databaseType, Optional tag, String dbHostString, String databaseName) { ConnectionUrl url = ConnectionUrl.newInstance(jdbcUrl); assertThat(url.getDatabaseType()).as("Database Type is as expected").isEqualTo(databaseType); assertThat(url.getImageTag()).as("Image tag is as expected").isEqualTo(tag); assertThat(url.getDbHostString()).as("Database Host String is as expected").isEqualTo(dbHostString); assertThat(url.getDatabaseName().orElse("")).as("Database Name is as expected").isEqualTo(databaseName); } } ================================================ FILE: modules/jdbc/src/test/java/org/testcontainers/jdbc/ConnectionUrlTest.java ================================================ package org.testcontainers.jdbc; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class ConnectionUrlTest { @Test void testConnectionUrl1() { String urlString = "jdbc:tc:mysql:8.0.36://somehostname:3306/databasename?a=b&c=d"; ConnectionUrl url = ConnectionUrl.newInstance(urlString); assertThat(url.getDatabaseType()).as("Database Type value is as expected").isEqualTo("mysql"); assertThat(url.getImageTag()).as("Database Image tag value is as expected").contains("8.0.36"); assertThat(url.getDbHostString()) .as("Database Host String is as expected") .isEqualTo("somehostname:3306/databasename"); assertThat(url.getQueryString()).as("Query String value is as expected").contains("?a=b&c=d"); assertThat(url.getDatabaseHost()).as("Database Host value is as expected").contains("somehostname"); assertThat(url.getDatabasePort()).as("Database Port value is as expected").contains(3306); assertThat(url.getDatabaseName()).as("Database Name value is as expected").contains("databasename"); assertThat(url.getQueryParameters()).as("Parameter a is captured").containsEntry("a", "b"); assertThat(url.getQueryParameters()).as("Parameter c is captured").containsEntry("c", "d"); } @Test void testConnectionUrl2() { String urlString = "jdbc:tc:mysql://somehostname/databasename"; ConnectionUrl url = ConnectionUrl.newInstance(urlString); assertThat(url.getDatabaseType()).as("Database Type value is as expected").isEqualTo("mysql"); assertThat(url.getImageTag()).as("Database Image tag value is as expected").isNotPresent(); assertThat(url.getDbHostString()) .as("Database Host String is as expected") .isEqualTo("somehostname/databasename"); assertThat(url.getQueryString()).as("Query String value is as expected").isEmpty(); assertThat(url.getDatabaseHost()).as("Database Host value is as expected").contains("somehostname"); assertThat(url.getDatabasePort()).as("Database Port is null as expected").isNotPresent(); assertThat(url.getDatabaseName()).as("Database Name value is as expected").contains("databasename"); assertThat(url.getQueryParameters().isEmpty()).as("Connection Parameters set is empty").isTrue(); } @Test void testEmptyQueryParameter() { ConnectionUrl url = ConnectionUrl.newInstance("jdbc:tc:mysql://somehostname/databasename?key="); assertThat(url.getQueryParameters().get("key")).as("'key' property value").isEqualTo(""); } @Test void testTmpfsOption() { String urlString = "jdbc:tc:mysql://somehostname/databasename?TC_TMPFS=key:value,key1:value1"; ConnectionUrl url = ConnectionUrl.newInstance(urlString); assertThat(url.getQueryParameters()).as("Connection Parameters set is empty").isEmpty(); assertThat(url.getContainerParameters()).as("Container Parameters set is not empty").isNotEmpty(); assertThat(url.getContainerParameters()) .as("Container Parameter TC_TMPFS is true") .containsEntry("TC_TMPFS", "key:value,key1:value1"); assertThat(url.getTmpfsOptions()).as("tmpfs option key has correct value").containsEntry("key", "value"); assertThat(url.getTmpfsOptions()).as("tmpfs option key1 has correct value").containsEntry("key1", "value1"); } @Test void testInitScriptPathCapture() { String urlString = "jdbc:tc:mysql:8.0.36://somehostname:3306/databasename?a=b&c=d&TC_INITSCRIPT=somepath/init_mysql.sql"; ConnectionUrl url = ConnectionUrl.newInstance(urlString); assertThat(url.getInitScriptPath()) .as("Database Type value is as expected") .contains("somepath/init_mysql.sql"); assertThat(url.getQueryString()).as("Query String value is as expected").contains("?a=b&c=d"); assertThat(url.getContainerParameters()) .as("INIT SCRIPT Path exists in Container Parameters") .containsEntry("TC_INITSCRIPT", "somepath/init_mysql.sql"); //Parameter sets are unmodifiable assertThatThrownBy(() -> url.getContainerParameters().remove("TC_INITSCRIPT")) .isInstanceOf(UnsupportedOperationException.class); assertThatThrownBy(() -> url.getQueryParameters().remove("a")) .isInstanceOf(UnsupportedOperationException.class); } @Test void testInitFunctionCapture() { String urlString = "jdbc:tc:mysql:8.0.36://somehostname:3306/databasename?a=b&c=d&TC_INITFUNCTION=org.testcontainers.jdbc.JDBCDriverTest::sampleInitFunction"; ConnectionUrl url = ConnectionUrl.newInstance(urlString); assertThat(url.getInitFunction()).as("Init Function parameter exists").isPresent(); assertThat(url.getInitFunction().get().getClassName()) .as("Init function class is as expected") .isEqualTo("org.testcontainers.jdbc.JDBCDriverTest"); assertThat(url.getInitFunction().get().getMethodName()) .as("Init function class is as expected") .isEqualTo("sampleInitFunction"); } @Test void testDaemonCapture() { String urlString = "jdbc:tc:mysql:8.0.36://somehostname:3306/databasename?a=b&c=d&TC_DAEMON=true"; ConnectionUrl url = ConnectionUrl.newInstance(urlString); assertThat(url.isInDaemonMode()).as("Daemon flag is set to true.").isTrue(); } @Test void testHostLessConnectionUrl() { String urlString = "jdbc:tc:mysql:8.0.36:///databasename?a=b&c=d"; ConnectionUrl url = ConnectionUrl.newInstance(urlString); assertThat(url.getDatabaseType()).as("Database Type value is as expected").isEqualTo("mysql"); assertThat(url.getImageTag()).as("Database Image tag value is as expected").contains("8.0.36"); assertThat(url.getQueryString()).as("Query String value is as expected").contains("?a=b&c=d"); assertThat(url.getDatabaseHost()).as("Database Host value is as expected").isEmpty(); assertThat(url.getDatabasePort()).as("Database Port value is as expected").isEmpty(); assertThat(url.getDatabaseName()).as("Database Name value is as expected").contains("databasename"); assertThat(url.getQueryParameters()).as("Parameter a is captured").containsEntry("a", "b"); assertThat(url.getQueryParameters()).as("Parameter c is captured").containsEntry("c", "d"); } } ================================================ FILE: modules/jdbc/src/test/java/org/testcontainers/jdbc/ContainerDatabaseDriverTest.java ================================================ package org.testcontainers.jdbc; import org.junit.jupiter.api.Test; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Properties; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class ContainerDatabaseDriverTest { private static final String PLAIN_POSTGRESQL_JDBC_URL = "jdbc:postgresql://localhost:5432/test"; @Test void shouldNotTryToConnectToNonMatchingJdbcUrlDirectly() throws SQLException { ContainerDatabaseDriver driver = new ContainerDatabaseDriver(); Connection connection = driver.connect(PLAIN_POSTGRESQL_JDBC_URL, new Properties()); assertThat(connection).isNull(); } @Test void shouldNotTryToConnectToNonMatchingJdbcUrlViaDriverManager() throws SQLException { assertThatThrownBy(() -> DriverManager.getConnection(PLAIN_POSTGRESQL_JDBC_URL)) .isInstanceOf(SQLException.class) .hasMessageStartingWith("No suitable driver found for "); } } ================================================ FILE: modules/jdbc/src/test/java/org/testcontainers/jdbc/JdbcDatabaseDelegateTest.java ================================================ package org.testcontainers.jdbc; import lombok.NonNull; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.slf4j.Logger; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.utility.DockerImageName; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class JdbcDatabaseDelegateTest { @Test void testLeakedConnections() { final JdbcDatabaseContainerStub stub = new JdbcDatabaseContainerStub(DockerImageName.parse("something")); try (JdbcDatabaseDelegate delegate = new JdbcDatabaseDelegate(stub, "")) { delegate.execute("foo", null, 0, false, false); } assertThat(stub.openConnectionsList.size()).isZero(); } static class JdbcDatabaseContainerStub extends JdbcDatabaseContainer { List openConnectionsList = new ArrayList<>(); public JdbcDatabaseContainerStub(@NonNull DockerImageName dockerImageName) { super(dockerImageName); } @Override public String getDriverClassName() { return null; } @Override public String getJdbcUrl() { return null; } @Override public String getUsername() { return null; } @Override public String getPassword() { return null; } @Override protected String getTestQueryString() { return null; } @Override public boolean isRunning() { return true; } @Override public Connection createConnection(String queryString) throws NoDriverFoundException, SQLException { final Connection connection = mock(Connection.class); openConnectionsList.add(connection); when(connection.createStatement()).thenReturn(mock(Statement.class)); connection.close(); Mockito.doAnswer(ignore -> openConnectionsList.remove(connection)).when(connection).close(); return connection; } @Override protected Logger logger() { return mock(Logger.class); } @Override public void setDockerImageName(@NonNull String dockerImageName) {} } } ================================================ FILE: modules/jdbc/src/test/java/org/testcontainers/jdbc/MissingJdbcDriverTest.java ================================================ package org.testcontainers.jdbc; import com.google.common.base.Throwables; import org.junit.jupiter.api.Test; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.utility.DockerImageName; import java.sql.Connection; import java.sql.SQLException; import java.util.concurrent.atomic.AtomicInteger; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; class MissingJdbcDriverTest { @Test void shouldFailFastIfNoDriverFound() { final MissingDriverContainer container = new MissingDriverContainer(); try { container.start(); fail("The container is expected to fail to start"); } catch (Exception e) { final Throwable rootCause = Throwables.getRootCause(e); assertThat(rootCause) .as("ClassNotFoundException is the root cause") .isInstanceOf(ClassNotFoundException.class); } finally { container.stop(); } assertThat(container.getConnectionAttempts()) .as("only one connection attempt should have been made") .isEqualTo(1); } /** * Container class for the purposes of testing, with a known non-existent driver */ static class MissingDriverContainer extends JdbcDatabaseContainer { private final AtomicInteger connectionAttempts = new AtomicInteger(); MissingDriverContainer() { super(DockerImageName.parse("mysql:8.0.36")); withEnv("MYSQL_ROOT_PASSWORD", "test"); withExposedPorts(3306); } @Override public String getDriverClassName() { return "nonexistent.ClassName"; } @Override public String getJdbcUrl() { return ""; } @Override public String getUsername() { return "root"; } @Override public String getPassword() { return "test"; } @Override protected String getTestQueryString() { return ""; } @Override public Connection createConnection(String queryString) throws SQLException, NoDriverFoundException { connectionAttempts.incrementAndGet(); // return super.createConnection(queryString); } /** * test window * @return how many times a connection was attempted */ int getConnectionAttempts() { return connectionAttempts.get(); } } } ================================================ FILE: modules/jdbc/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/jdbc-test/build.gradle ================================================ dependencies { api project(':testcontainers-jdbc') api 'com.google.guava:guava:33.5.0-jre' api 'org.apache.commons:commons-lang3:3.20.0' api 'com.zaxxer:HikariCP-java6:2.3.13' api 'commons-dbutils:commons-dbutils:1.8.1' api 'org.assertj:assertj-core:3.27.6' api 'org.apache.tomcat:tomcat-jdbc:11.0.14' api 'org.vibur:vibur-dbcp:26.0' api 'com.mysql:mysql-connector-j:9.5.0' api 'org.junit.jupiter:junit-jupiter:5.14.1' } ================================================ FILE: modules/jdbc-test/sql/init_mysql.sql ================================================ CREATE TABLE bar ( foo VARCHAR(255) ); INSERT INTO bar (foo) VALUES ('hello world'); ================================================ FILE: modules/jdbc-test/src/main/java/org/testcontainers/db/AbstractContainerDatabaseTest.java ================================================ package org.testcontainers.db; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import org.testcontainers.containers.JdbcDatabaseContainer; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import javax.sql.DataSource; public abstract class AbstractContainerDatabaseTest { protected ResultSet performQuery(JdbcDatabaseContainer container, String sql) throws SQLException { DataSource ds = getDataSource(container); Statement statement = ds.getConnection().createStatement(); statement.execute(sql); ResultSet resultSet = statement.getResultSet(); resultSet.next(); return resultSet; } protected DataSource getDataSource(JdbcDatabaseContainer container) { HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setJdbcUrl(container.getJdbcUrl()); hikariConfig.setUsername(container.getUsername()); hikariConfig.setPassword(container.getPassword()); hikariConfig.setDriverClassName(container.getDriverClassName()); return new HikariDataSource(hikariConfig); } } ================================================ FILE: modules/jdbc-test/src/main/java/org/testcontainers/jdbc/AbstractJDBCDriverTest.java ================================================ package org.testcontainers.jdbc; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.lang3.SystemUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.Parameter; import org.junit.jupiter.params.ParameterizedClass; import org.junit.jupiter.params.provider.MethodSource; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.EnumSet; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assumptions.assumeThat; @ParameterizedClass @MethodSource("data") public class AbstractJDBCDriverTest { protected enum Options { ScriptedSchema, CharacterSet, CustomIniFile, JDBCParams, PmdKnownBroken, } @Parameter(0) public String jdbcUrl; @Parameter(1) public EnumSet options; public static void sampleInitFunction(Connection connection) throws SQLException { connection.createStatement().execute("CREATE TABLE bar (\n" + " foo VARCHAR(255)\n" + ");"); connection.createStatement().execute("INSERT INTO bar (foo) VALUES ('hello world');"); connection.createStatement().execute("CREATE TABLE my_counter (\n" + " n INT\n" + ");"); } @AfterAll public static void testCleanup() { ContainerDatabaseDriver.killContainers(); } @Test void test() throws SQLException { try (HikariDataSource dataSource = getDataSource(jdbcUrl, 1)) { performSimpleTest(dataSource); if (options.contains(Options.ScriptedSchema)) { performTestForScriptedSchema(dataSource); } if (options.contains(Options.JDBCParams)) { performTestForJDBCParamUsage(dataSource); } if (options.contains(Options.CharacterSet)) { performSimpleTestWithCharacterSet(jdbcUrl); performTestForCharacterEncodingForInitialScriptConnection(dataSource); } if (options.contains(Options.CustomIniFile)) { performTestForCustomIniFile(dataSource); } } } private void performSimpleTest(HikariDataSource dataSource) throws SQLException { String query = "SELECT 1"; if (jdbcUrl.startsWith("jdbc:tc:db2:")) { query = "SELECT 1 FROM SYSIBM.SYSDUMMY1"; } boolean result = new QueryRunner(dataSource, options.contains(Options.PmdKnownBroken)) .query( query, rs -> { rs.next(); int resultSetInt = rs.getInt(1); assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); return true; } ); assertThat(result).as("The database returned a record as expected").isTrue(); } private void performTestForScriptedSchema(HikariDataSource dataSource) throws SQLException { boolean result = new QueryRunner(dataSource) .query( "SELECT foo FROM bar WHERE foo LIKE '%world'", rs -> { rs.next(); String resultSetString = rs.getString(1); assertThat(resultSetString) .as("A basic SELECT query succeeds where the schema has been applied from a script") .isEqualTo("hello world"); return true; } ); assertThat(result).as("The database returned a record as expected").isTrue(); } private void performTestForJDBCParamUsage(HikariDataSource dataSource) throws SQLException { boolean result = new QueryRunner(dataSource) .query( "select CURRENT_USER", rs -> { rs.next(); String resultUser = rs.getString(1); // Not all databases (eg. Postgres) return @% at the end of user name. We just need to make sure the user name matches. if (resultUser.endsWith("@%")) { resultUser = resultUser.substring(0, resultUser.length() - 2); } assertThat(resultUser).as("User from query param is created.").isEqualTo("someuser"); return true; } ); assertThat(result).as("The database returned a record as expected").isTrue(); String databaseQuery = "SELECT DATABASE()"; // Postgres does not have Database() as a function String databaseType = ConnectionUrl.newInstance(jdbcUrl).getDatabaseType(); if ( databaseType.equalsIgnoreCase("postgresql") || databaseType.equalsIgnoreCase("postgis") || databaseType.equalsIgnoreCase("timescaledb") || databaseType.equalsIgnoreCase("pgvector") ) { databaseQuery = "SELECT CURRENT_DATABASE()"; } result = new QueryRunner(dataSource) .query( databaseQuery, rs -> { rs.next(); String resultDB = rs.getString(1); assertThat(resultDB).as("Database name from URL String is used.").isEqualTo("databasename"); return true; } ); assertThat(result).as("The database returned a record as expected").isTrue(); } private void performTestForCharacterEncodingForInitialScriptConnection(HikariDataSource dataSource) throws SQLException { boolean result = new QueryRunner(dataSource) .query( "SELECT foo FROM bar WHERE foo LIKE '%мир'", rs -> { rs.next(); String resultSetString = rs.getString(1); assertThat(resultSetString) .as("A basic SELECT query succeeds where the schema has been applied from a script") .isEqualTo("привет мир"); return true; } ); assertThat(result).as("The database returned a record as expected").isTrue(); } /** * This method intentionally verifies encoding twice to ensure that the query string parameters are used when * Connections are created from cached containers. * * @param jdbcUrl * @throws SQLException */ private void performSimpleTestWithCharacterSet(String jdbcUrl) throws SQLException { HikariDataSource datasource1 = verifyCharacterSet(jdbcUrl); HikariDataSource datasource2 = verifyCharacterSet(jdbcUrl); datasource1.close(); datasource2.close(); } private HikariDataSource verifyCharacterSet(String jdbcUrl) throws SQLException { HikariDataSource dataSource = getDataSource(jdbcUrl, 1); boolean result = new QueryRunner(dataSource) .query( "SHOW VARIABLES LIKE 'character\\_set\\_connection'", rs -> { rs.next(); String resultSetString = rs.getString(2); assertThat(resultSetString) .as("Passing query parameters to set DB connection encoding is successful") .startsWith("utf8"); return true; } ); assertThat(result).as("The database returned a record as expected").isTrue(); return dataSource; } private void performTestForCustomIniFile(HikariDataSource dataSource) throws SQLException { assumeThat(SystemUtils.IS_OS_WINDOWS).isFalse(); Statement statement = dataSource.getConnection().createStatement(); statement.execute("SELECT @@GLOBAL.innodb_max_undo_log_size"); ResultSet resultSet = statement.getResultSet(); assertThat(resultSet.next()).as("The query returns a result").isTrue(); long result = resultSet.getLong(1); assertThat(result).as("The InnoDB max undo log size has been set by the ini file content").isEqualTo(20000000); } private HikariDataSource getDataSource(String jdbcUrl, int poolSize) { HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setJdbcUrl(jdbcUrl); hikariConfig.setConnectionTestQuery("SELECT 1"); hikariConfig.setMinimumIdle(1); hikariConfig.setMaximumPoolSize(poolSize); return new HikariDataSource(hikariConfig); } } ================================================ FILE: modules/jdbc-test/src/main/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/junit-jupiter/build.gradle ================================================ description = "Testcontainers :: JUnit Jupiter Extension" dependencies { api project(':testcontainers') implementation platform('org.junit:junit-bom:5.14.1') implementation 'org.junit.jupiter:junit-jupiter-api' testImplementation project(':testcontainers-mysql') testImplementation project(':testcontainers-postgresql') testImplementation 'com.zaxxer:HikariCP:7.0.2' testImplementation 'redis.clients:jedis:7.1.0' testImplementation 'org.apache.httpcomponents:httpclient:4.5.14' testImplementation ('org.mockito:mockito-core:4.11.0') { exclude(module: 'hamcrest-core') } testRuntimeOnly 'org.postgresql:postgresql:42.7.8' testRuntimeOnly 'com.mysql:mysql-connector-j:9.5.0' } ================================================ FILE: modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/Container.java ================================================ package org.testcontainers.junit.jupiter; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * The {@code @Container} annotation is used in conjunction with the {@link Testcontainers} annotation * to mark containers that should be managed by the Testcontainers extension. * * @see Testcontainers */ @Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface Container { } ================================================ FILE: modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/DockerAvailableDetector.java ================================================ package org.testcontainers.junit.jupiter; import org.testcontainers.DockerClientFactory; class DockerAvailableDetector { public boolean isDockerAvailable() { try { DockerClientFactory.instance().client(); return true; } catch (Throwable ex) { return false; } } } ================================================ FILE: modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/EnabledIfDockerAvailable.java ================================================ package org.testcontainers.junit.jupiter; import org.junit.jupiter.api.extension.ExtendWith; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * {@code EnabledIfDockerAvailable} is a JUnit Jupiter extension to enable tests only if Docker is available. */ @Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @ExtendWith(EnabledIfDockerAvailableCondition.class) public @interface EnabledIfDockerAvailable { } ================================================ FILE: modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/EnabledIfDockerAvailableCondition.java ================================================ package org.testcontainers.junit.jupiter; import org.junit.jupiter.api.extension.ConditionEvaluationResult; import org.junit.jupiter.api.extension.ExecutionCondition; import org.junit.jupiter.api.extension.ExtensionConfigurationException; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.platform.commons.support.AnnotationSupport; import java.util.Optional; class EnabledIfDockerAvailableCondition implements ExecutionCondition { private final DockerAvailableDetector dockerDetector = new DockerAvailableDetector(); @Override public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { return findAnnotation(context) .map(this::evaluate) .orElseThrow(() -> new ExtensionConfigurationException("@EnabledIfDockerAvailable not found")); } boolean isDockerAvailable() { return this.dockerDetector.isDockerAvailable(); } private ConditionEvaluationResult evaluate(EnabledIfDockerAvailable testcontainers) { if (isDockerAvailable()) { return ConditionEvaluationResult.enabled("Docker is available"); } return ConditionEvaluationResult.disabled("Docker is not available"); } private Optional findAnnotation(ExtensionContext context) { Optional current = Optional.of(context); while (current.isPresent()) { Optional enabledIfDockerAvailable = AnnotationSupport.findAnnotation( current.get().getRequiredTestClass(), EnabledIfDockerAvailable.class ); if (enabledIfDockerAvailable.isPresent()) { return enabledIfDockerAvailable; } current = current.get().getParent(); } return Optional.empty(); } } ================================================ FILE: modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/FilesystemFriendlyNameGenerator.java ================================================ package org.testcontainers.junit.jupiter; import org.junit.jupiter.api.extension.ExtensionContext; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; class FilesystemFriendlyNameGenerator { private static final String UNKNOWN_NAME = "unknown"; static String filesystemFriendlyNameOf(ExtensionContext context) { String contextId = context.getUniqueId(); try { return (contextId == null || contextId.trim().isEmpty()) ? UNKNOWN_NAME : URLEncoder.encode(contextId, StandardCharsets.UTF_8.toString()); } catch (UnsupportedEncodingException e) { return UNKNOWN_NAME; } } } ================================================ FILE: modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/Testcontainers.java ================================================ package org.testcontainers.junit.jupiter; import org.junit.jupiter.api.extension.ExtendWith; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * {@code @Testcontainers} is a JUnit Jupiter extension to activate automatic * startup and stop of containers used in a test case. * *

The Testcontainers extension finds all fields that are annotated with * {@link Container} and calls their container lifecycle methods. Containers * declared as static fields will be shared between test methods. They will be * started only once before any test method is executed and stopped after the * last test method has executed. Containers declared as instance fields will * be started and stopped for every test method.

* *

The annotation {@code @Testcontainers} can be used on a superclass in * the test hierarchy as well. All subclasses will automatically inherit * support for the extension.

* *

Note: This extension has only been tested with sequential * test execution. Using it with parallel test execution is unsupported and * may have unintended side effects.

* *

Example:

* *
 * @Testcontainers
 * class MyTestcontainersTests {
 *
 *     // will be shared between test methods
 *     @Container
 *     private static final MySQLContainer MY_SQL_CONTAINER = new MySQLContainer();
 *
 *     // will be started before and stopped after each test method
 *     @Container
 *     private PostgreSQLContainer postgresqlContainer = new PostgreSQLContainer()
 *             .withDatabaseName("foo")
 *             .withUsername("foo")
 *             .withPassword("secret");
 *
 *     @Test
 *     void test() {
 *         assertTrue(MY_SQL_CONTAINER.isRunning());
 *         assertTrue(postgresqlContainer.isRunning());
 *     }
 * }
 * 
* * @see Container */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @ExtendWith(TestcontainersExtension.class) @Inherited public @interface Testcontainers { /** * Whether tests should be disabled (rather than failing) when Docker is not available. Defaults to * {@code false}. * @return if the tests should be disabled when Docker is not available */ boolean disabledWithoutDocker() default false; /** * Whether containers should start in parallel. Defaults to {@code false}. * @return if the containers should start in parallel */ boolean parallel() default false; } ================================================ FILE: modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/TestcontainersExtension.java ================================================ package org.testcontainers.junit.jupiter; import lombok.Getter; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ConditionEvaluationResult; import org.junit.jupiter.api.extension.ExecutionCondition; import org.junit.jupiter.api.extension.ExtensionConfigurationException; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext.Namespace; import org.junit.jupiter.api.extension.ExtensionContext.Store; import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; import org.junit.platform.commons.support.AnnotationSupport; import org.junit.platform.commons.support.HierarchyTraversalMode; import org.junit.platform.commons.support.ModifierSupport; import org.junit.platform.commons.support.ReflectionSupport; import org.testcontainers.lifecycle.Startable; import org.testcontainers.lifecycle.Startables; import org.testcontainers.lifecycle.TestDescription; import org.testcontainers.lifecycle.TestLifecycleAware; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; public class TestcontainersExtension implements BeforeEachCallback, BeforeAllCallback, AfterEachCallback, AfterAllCallback, ExecutionCondition { private static final Namespace NAMESPACE = Namespace.create(TestcontainersExtension.class); private static final String SHARED_LIFECYCLE_AWARE_CONTAINERS = "sharedLifecycleAwareContainers"; private static final String LOCAL_LIFECYCLE_AWARE_CONTAINERS = "localLifecycleAwareContainers"; private final DockerAvailableDetector dockerDetector = new DockerAvailableDetector(); @Override public void beforeAll(ExtensionContext context) { Class testClass = context .getTestClass() .orElseThrow(() -> { return new ExtensionConfigurationException("TestcontainersExtension is only supported for classes."); }); Store store = context.getStore(NAMESPACE); List sharedContainersStoreAdapters = findSharedContainers(testClass); startContainers(sharedContainersStoreAdapters, store, context); List lifecycleAwareContainers = sharedContainersStoreAdapters .stream() .filter(this::isTestLifecycleAware) .map(lifecycleAwareAdapter -> (TestLifecycleAware) lifecycleAwareAdapter.container) .collect(Collectors.toList()); store.put(SHARED_LIFECYCLE_AWARE_CONTAINERS, lifecycleAwareContainers); signalBeforeTestToContainers(lifecycleAwareContainers, testDescriptionFrom(context)); } private void startContainers(List storeAdapters, Store store, ExtensionContext context) { if (storeAdapters.isEmpty()) { return; } if (isParallelExecutionEnabled(context)) { Stream startables = storeAdapters .stream() .map(storeAdapter -> { store.getOrComputeIfAbsent(storeAdapter.getKey(), k -> storeAdapter); return storeAdapter.container; }); Startables.deepStart(startables).join(); } else { storeAdapters.forEach(adapter -> store.getOrComputeIfAbsent(adapter.getKey(), k -> adapter.start())); } } @Override public void afterAll(ExtensionContext context) { signalAfterTestToContainersFor(SHARED_LIFECYCLE_AWARE_CONTAINERS, context); } @Override public void beforeEach(final ExtensionContext context) { Store store = context.getStore(NAMESPACE); List restartContainers = collectParentTestInstances(context) .parallelStream() .flatMap(this::findRestartContainers) .collect(Collectors.toList()); List lifecycleAwareContainers = findTestLifecycleAwareContainers( restartContainers, store, context ); store.put(LOCAL_LIFECYCLE_AWARE_CONTAINERS, lifecycleAwareContainers); signalBeforeTestToContainers(lifecycleAwareContainers, testDescriptionFrom(context)); } private List findTestLifecycleAwareContainers( List restartContainers, Store store, ExtensionContext context ) { startContainers(restartContainers, store, context); return restartContainers .stream() .filter(this::isTestLifecycleAware) .map(lifecycleAwareAdapter -> (TestLifecycleAware) lifecycleAwareAdapter.container) .collect(Collectors.toList()); } private boolean isParallelExecutionEnabled(ExtensionContext context) { return findTestcontainers(context).map(Testcontainers::parallel).orElse(false); } @Override public void afterEach(ExtensionContext context) { signalAfterTestToContainersFor(LOCAL_LIFECYCLE_AWARE_CONTAINERS, context); } private void signalBeforeTestToContainers( List lifecycleAwareContainers, TestDescription testDescription ) { lifecycleAwareContainers.forEach(container -> container.beforeTest(testDescription)); } private void signalAfterTestToContainersFor(String storeKey, ExtensionContext context) { List lifecycleAwareContainers = (List) context .getStore(NAMESPACE) .get(storeKey); if (lifecycleAwareContainers != null) { TestDescription description = testDescriptionFrom(context); Optional throwable = context.getExecutionException(); lifecycleAwareContainers.forEach(container -> container.afterTest(description, throwable)); } } private TestDescription testDescriptionFrom(ExtensionContext context) { return new TestcontainersTestDescription( context.getUniqueId(), FilesystemFriendlyNameGenerator.filesystemFriendlyNameOf(context) ); } private boolean isTestLifecycleAware(StoreAdapter adapter) { return adapter.container instanceof TestLifecycleAware; } @Override public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { return findTestcontainers(context) .map(this::evaluate) .orElseThrow(() -> new ExtensionConfigurationException("@Testcontainers not found")); } private Optional findTestcontainers(ExtensionContext context) { Optional current = Optional.of(context); while (current.isPresent()) { Optional testcontainers = AnnotationSupport.findAnnotation( current.get().getRequiredTestClass(), Testcontainers.class ); if (testcontainers.isPresent()) { return testcontainers; } current = current.get().getParent(); } return Optional.empty(); } private ConditionEvaluationResult evaluate(Testcontainers testcontainers) { if (testcontainers.disabledWithoutDocker()) { if (isDockerAvailable()) { return ConditionEvaluationResult.enabled("Docker is available"); } return ConditionEvaluationResult.disabled("disabledWithoutDocker is true and Docker is not available"); } return ConditionEvaluationResult.enabled("disabledWithoutDocker is false"); } boolean isDockerAvailable() { return this.dockerDetector.isDockerAvailable(); } private Set collectParentTestInstances(final ExtensionContext context) { List allInstances = new ArrayList<>(context.getRequiredTestInstances().getAllInstances()); Collections.reverse(allInstances); return new LinkedHashSet<>(allInstances); } private List findSharedContainers(Class testClass) { return ReflectionSupport .findFields(testClass, isSharedContainer(), HierarchyTraversalMode.TOP_DOWN) .stream() .map(f -> getContainerInstance(null, f)) .collect(Collectors.toList()); } private Predicate isSharedContainer() { return isContainer().and(ModifierSupport::isStatic); } private Stream findRestartContainers(Object testInstance) { return ReflectionSupport .findFields(testInstance.getClass(), isRestartContainer(), HierarchyTraversalMode.TOP_DOWN) .stream() .map(f -> getContainerInstance(testInstance, f)); } private Predicate isRestartContainer() { return isContainer().and(ModifierSupport::isNotStatic); } private static Predicate isContainer() { return field -> { boolean isAnnotatedWithContainer = AnnotationSupport.isAnnotated(field, Container.class); if (isAnnotatedWithContainer) { boolean isStartable = Startable.class.isAssignableFrom(field.getType()); if (!isStartable) { throw new ExtensionConfigurationException( String.format("FieldName: %s does not implement Startable", field.getName()) ); } return true; } return false; }; } private static StoreAdapter getContainerInstance(final Object testInstance, final Field field) { try { field.setAccessible(true); Startable containerInstance = (Startable) field.get(testInstance); if (containerInstance == null) { throw new ExtensionConfigurationException("Container " + field.getName() + " needs to be initialized"); } return new StoreAdapter(field.getDeclaringClass(), field.getName(), containerInstance); } catch (IllegalAccessException e) { throw new ExtensionConfigurationException("Can not access container defined in field " + field.getName()); } } /** * An adapter for {@link Startable} that implement {@link CloseableResource} * thereby letting the JUnit automatically stop containers once the current * {@link ExtensionContext} is closed. */ private static class StoreAdapter implements CloseableResource, AutoCloseable { @Getter private String key; private Startable container; private StoreAdapter(Class declaringClass, String fieldName, Startable container) { this.key = declaringClass.getName() + "." + fieldName; this.container = container; } private StoreAdapter start() { container.start(); return this; } @Override public void close() { container.stop(); } } } ================================================ FILE: modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/TestcontainersTestDescription.java ================================================ package org.testcontainers.junit.jupiter; import lombok.Value; import org.testcontainers.lifecycle.TestDescription; @Value class TestcontainersTestDescription implements TestDescription { String testId; String filesystemFriendlyName; } ================================================ FILE: modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/ComposeContainerTests.java ================================================ package org.testcontainers.junit.jupiter; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.HttpClientBuilder; import org.junit.jupiter.api.Test; import org.testcontainers.containers.ComposeContainer; import org.testcontainers.containers.wait.strategy.Wait; import java.io.File; import static org.assertj.core.api.Assertions.assertThat; @Testcontainers class ComposeContainerTests { @Container private ComposeContainer composeContainer = new ComposeContainer(new File("src/test/resources/docker-compose.yml")) .withExposedService("whoami-1", 80, Wait.forHttp("/")); @Test void running_compose_defined_container_is_accessible_on_configured_port() throws Exception { HttpClient client = HttpClientBuilder.create().build(); String host = composeContainer.getServiceHost("whoami-1", 80); int port = composeContainer.getServicePort("whoami-1", 80); HttpResponse response = client.execute(new HttpGet("http://" + host + ":" + port)); assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200); } } ================================================ FILE: modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/DockerComposeContainerTests.java ================================================ package org.testcontainers.junit.jupiter; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.HttpClientBuilder; import org.junit.jupiter.api.Test; import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import java.io.File; import static org.assertj.core.api.Assertions.assertThat; @Testcontainers class DockerComposeContainerTests { @Container private DockerComposeContainer composeContainer = new DockerComposeContainer( DockerImageName.parse("docker/compose:1.29.2"), new File("src/test/resources/docker-compose.yml") ) .withExposedService("whoami_1", 80, Wait.forHttp("/")); @Test void running_compose_defined_container_is_accessible_on_configured_port() throws Exception { HttpClient client = HttpClientBuilder.create().build(); String host = composeContainer.getServiceHost("whoami_1", 80); int port = composeContainer.getServicePort("whoami_1", 80); HttpResponse response = client.execute(new HttpGet("http://" + host + ":" + port)); assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200); } } ================================================ FILE: modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/EnabledIfDockerAvailableTests.java ================================================ package org.testcontainers.junit.jupiter; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ConditionEvaluationResult; import org.junit.jupiter.api.extension.ExtensionContext; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class EnabledIfDockerAvailableTests { @Test void whenDockerIsAvailableTestsAreEnabled() { ConditionEvaluationResult result = new TestEnabledIfDockerAvailableCondition(true) .evaluateExecutionCondition(extensionContext(DisabledWithoutDocker.class)); assertThat(result.isDisabled()).isFalse(); } @Test void whenDockerIsUnavailableTestsAreDisabled() { ConditionEvaluationResult result = new TestEnabledIfDockerAvailableCondition(false) .evaluateExecutionCondition(extensionContext(DisabledWithoutDocker.class)); assertThat(result.isDisabled()).isTrue(); } private ExtensionContext extensionContext(Class clazz) { ExtensionContext extensionContext = mock(ExtensionContext.class); when(extensionContext.getRequiredTestClass()).thenReturn(clazz); return extensionContext; } @EnabledIfDockerAvailable static final class DisabledWithoutDocker {} static final class TestEnabledIfDockerAvailableCondition extends EnabledIfDockerAvailableCondition { private final boolean dockerAvailable; private TestEnabledIfDockerAvailableCondition(boolean dockerAvailable) { this.dockerAvailable = dockerAvailable; } boolean isDockerAvailable() { return dockerAvailable; } } } ================================================ FILE: modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/FilesystemFriendlyNameGeneratorTest.java ================================================ package org.testcontainers.junit.jupiter; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; class FilesystemFriendlyNameGeneratorTest { @ParameterizedTest @MethodSource("provideDisplayNamesAndFilesystemFriendlyNames") void should_generate_filesystem_friendly_name(String displayName, String expectedName) { ExtensionContext context = mock(ExtensionContext.class); doReturn(displayName).when(context).getUniqueId(); String filesystemFriendlyName = FilesystemFriendlyNameGenerator.filesystemFriendlyNameOf(context); assertThat(filesystemFriendlyName).isEqualTo(expectedName); } private static Stream provideDisplayNamesAndFilesystemFriendlyNames() { return Stream.of( Arguments.of("", "unknown"), Arguments.of(" ", "unknown"), Arguments.of("not blank", "not+blank"), Arguments.of("abc ABC 1234567890", "abc+ABC+1234567890"), Arguments.of( "no_umlauts_äöüÄÖÜéáíó", "no_umlauts_%C3%A4%C3%B6%C3%BC%C3%84%C3%96%C3%9C%C3%A9%C3%A1%C3%AD%C3%B3" ), Arguments.of( "[engine:junit-jupiter]/[class:com.example.MyTest]/[test-factory:parameterizedTest()]/[dynamic-test:#3]", "%5Bengine%3Ajunit-jupiter%5D%2F%5Bclass%3Acom.example.MyTest%5D%2F%5Btest-factory%3AparameterizedTest%28%29%5D%2F%5Bdynamic-test%3A%233%5D" ) ); } } ================================================ FILE: modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/JUnitJupiterTestImages.java ================================================ package org.testcontainers.junit.jupiter; import org.testcontainers.utility.DockerImageName; public interface JUnitJupiterTestImages { DockerImageName POSTGRES_IMAGE = DockerImageName.parse("postgres:9.6.12"); DockerImageName HTTPD_IMAGE = DockerImageName.parse("httpd:2.4-alpine"); DockerImageName MYSQL_IMAGE = DockerImageName.parse("mysql:8.0.32"); } ================================================ FILE: modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/MetaAnnotationTest.java ================================================ package org.testcontainers.junit.jupiter; import org.junit.jupiter.api.Test; import org.testcontainers.containers.PostgreSQLContainer; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import static org.assertj.core.api.Assertions.assertThat; @Testcontainers class MetaAnnotationTest { @TcContainer private static final PostgreSQLContainer POSTGRESQL = new PostgreSQLContainer<>( JUnitJupiterTestImages.POSTGRES_IMAGE ); @Test void test() { assertThat(POSTGRESQL.isRunning()).isTrue(); } } @Container @Retention(RetentionPolicy.RUNTIME) @interface TcContainer { } ================================================ FILE: modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/MixedLifecycleTests.java ================================================ package org.testcontainers.junit.jupiter; import org.junit.jupiter.api.Test; import org.testcontainers.containers.MySQLContainer; import org.testcontainers.containers.PostgreSQLContainer; import static org.assertj.core.api.Assertions.assertThat; // testClass { @Testcontainers class MixedLifecycleTests { // will be shared between test methods @Container private static final MySQLContainer MY_SQL_CONTAINER = new MySQLContainer("mysql:8.0.36"); // will be started before and stopped after each test method @Container private PostgreSQLContainer postgresqlContainer = new PostgreSQLContainer("postgres:9.6.12") .withDatabaseName("foo") .withUsername("foo") .withPassword("secret"); @Test void test() { assertThat(MY_SQL_CONTAINER.isRunning()).isTrue(); assertThat(postgresqlContainer.isRunning()).isTrue(); } } // } ================================================ FILE: modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/ParallelExecutionTests.java ================================================ package org.testcontainers.junit.jupiter; import org.junit.jupiter.api.Test; import org.testcontainers.containers.MySQLContainer; import org.testcontainers.containers.PostgreSQLContainer; import static org.assertj.core.api.Assertions.assertThat; @Testcontainers(parallel = true) public class ParallelExecutionTests { @Container private static final PostgreSQLContainer POSTGRESQL_CONTAINER = new PostgreSQLContainer<>( JUnitJupiterTestImages.POSTGRES_IMAGE ) .withDatabaseName("foo") .withUsername("foo") .withPassword("secret"); @Container private MySQLContainer mySQLContainer = new MySQLContainer<>(JUnitJupiterTestImages.MYSQL_IMAGE); @Test void test() { assertThat(POSTGRESQL_CONTAINER.isRunning()).isTrue(); assertThat(mySQLContainer.isRunning()).isTrue(); } } ================================================ FILE: modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/PostgresContainerTests.java ================================================ package org.testcontainers.junit.jupiter; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import org.junit.jupiter.api.Test; import org.testcontainers.containers.PostgreSQLContainer; import java.sql.ResultSet; import java.sql.Statement; import static org.assertj.core.api.Assertions.assertThat; @Testcontainers class PostgresContainerTests { @Container private static final PostgreSQLContainer POSTGRE_SQL_CONTAINER = new PostgreSQLContainer<>( JUnitJupiterTestImages.POSTGRES_IMAGE ) .withDatabaseName("foo") .withUsername("foo") .withPassword("secret"); @Test void waits_until_postgres_accepts_jdbc_connections() throws Exception { HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setJdbcUrl(POSTGRE_SQL_CONTAINER.getJdbcUrl()); hikariConfig.setUsername("foo"); hikariConfig.setPassword("secret"); try (HikariDataSource ds = new HikariDataSource(hikariConfig)) { Statement statement = ds.getConnection().createStatement(); statement.execute("SELECT 1"); ResultSet resultSet = statement.getResultSet(); resultSet.next(); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).isEqualTo(1); } } } ================================================ FILE: modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/TestLifecycleAwareContainerMock.java ================================================ package org.testcontainers.junit.jupiter; import org.testcontainers.lifecycle.Startable; import org.testcontainers.lifecycle.TestDescription; import org.testcontainers.lifecycle.TestLifecycleAware; import java.util.ArrayList; import java.util.List; import java.util.Optional; public class TestLifecycleAwareContainerMock implements Startable, TestLifecycleAware { static final String BEFORE_TEST = "beforeTest"; static final String AFTER_TEST = "afterTest"; private final List lifecycleMethodCalls = new ArrayList<>(); private final List lifecycleFilesystemFriendlyNames = new ArrayList<>(); private Throwable capturedThrowable; @Override public void beforeTest(TestDescription description) { lifecycleMethodCalls.add(BEFORE_TEST); lifecycleFilesystemFriendlyNames.add(description.getFilesystemFriendlyName()); } @Override public void afterTest(TestDescription description, Optional throwable) { lifecycleMethodCalls.add(AFTER_TEST); throwable.ifPresent(capturedThrowable -> this.capturedThrowable = capturedThrowable); } List getLifecycleMethodCalls() { return lifecycleMethodCalls; } Throwable getCapturedThrowable() { return capturedThrowable; } public List getLifecycleFilesystemFriendlyNames() { return lifecycleFilesystemFriendlyNames; } @Override public void start() {} @Override public void stop() {} } ================================================ FILE: modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/TestLifecycleAwareExceptionCapturingTest.java ================================================ package org.testcontainers.junit.jupiter; import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import org.opentest4j.TestAbortedException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assumptions.assumeThat; // The order of @ExtendsWith and @Testcontainers is crucial in order for the tests @Testcontainers @TestMethodOrder(OrderAnnotation.class) class TestLifecycleAwareExceptionCapturingTest { @Container private final TestLifecycleAwareContainerMock testContainer = new TestLifecycleAwareContainerMock(); private static TestLifecycleAwareContainerMock startedTestContainer; @Test @Order(1) void failing_test_should_pass_throwable_to_testContainer() { startedTestContainer = testContainer; // Force an exception that is captured by the test container without failing the test itself assumeThat(false).isTrue(); } @Test @Order(2) void should_have_captured_thrownException() { Throwable capturedThrowable = startedTestContainer.getCapturedThrowable(); assertThat(capturedThrowable).isInstanceOf(TestAbortedException.class); assertThat(capturedThrowable.getMessage()).contains("Expecting value to be true but was false"); } } ================================================ FILE: modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/TestLifecycleAwareMethodTest.java ================================================ package org.testcontainers.junit.jupiter; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtensionContext; import static org.assertj.core.api.Assertions.assertThat; // The order of @ExtendsWith and @Testcontainers is crucial for the tests @ExtendWith({ TestLifecycleAwareMethodTest.SharedContainerAfterAllTestExtension.class }) @Testcontainers @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class TestLifecycleAwareMethodTest { @Container private final TestLifecycleAwareContainerMock testContainer = new TestLifecycleAwareContainerMock(); @Container private static final TestLifecycleAwareContainerMock SHARED_CONTAINER = new TestLifecycleAwareContainerMock(); private static TestLifecycleAwareContainerMock startedTestContainer; @BeforeAll static void beforeAll() { assertThat(SHARED_CONTAINER.getLifecycleMethodCalls()) .containsExactly(TestLifecycleAwareContainerMock.BEFORE_TEST); } @Test @Order(1) void should_prepare_before_and_after_test() { // we can only test for a call to afterTest() after this test has been finished. startedTestContainer = testContainer; } @Test @Order(2) void should_call_beforeTest_first_afterTest_later_with_filesystem_friendly_name() { assertThat(startedTestContainer.getLifecycleMethodCalls()) .containsExactly(TestLifecycleAwareContainerMock.BEFORE_TEST, TestLifecycleAwareContainerMock.AFTER_TEST); } @Test void should_have_a_filesystem_friendly_name_container_has_started() { assertThat(startedTestContainer.getLifecycleFilesystemFriendlyNames()) .containsExactly( "%5Bengine%3Ajunit-jupiter%5D%2F%5Bclass%3Aorg.testcontainers.junit.jupiter.TestLifecycleAwareMethodTest%5D%2F%5Bmethod%3Ashould_prepare_before_and_after_test%28%29%5D" ); } @Test void static_container_should_have_a_filesystem_friendly_name_after_container_has_started() { assertThat(SHARED_CONTAINER.getLifecycleFilesystemFriendlyNames()) .containsExactly( "%5Bengine%3Ajunit-jupiter%5D%2F%5Bclass%3Aorg.testcontainers.junit.jupiter.TestLifecycleAwareMethodTest%5D" ); } static class SharedContainerAfterAllTestExtension implements AfterAllCallback { // Unfortunately it's not possible to write a @Test that is run after all tests @Override public void afterAll(ExtensionContext context) { assertThat(SHARED_CONTAINER.getLifecycleMethodCalls()) .containsExactly( TestLifecycleAwareContainerMock.BEFORE_TEST, TestLifecycleAwareContainerMock.AFTER_TEST ); } } } ================================================ FILE: modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/TestcontainersExtensionTests.java ================================================ package org.testcontainers.junit.jupiter; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ConditionEvaluationResult; import org.junit.jupiter.api.extension.ExtensionContext; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class TestcontainersExtensionTests { @Test void whenDisabledWithoutDockerAndDockerIsAvailableTestsAreEnabled() { ConditionEvaluationResult result = new TestTestcontainersExtension(true) .evaluateExecutionCondition(extensionContext(DisabledWithoutDocker.class)); assertThat(result.isDisabled()).isFalse(); } @Test void whenDisabledWithoutDockerAndDockerIsUnavailableTestsAreDisabled() { ConditionEvaluationResult result = new TestTestcontainersExtension(false) .evaluateExecutionCondition(extensionContext(DisabledWithoutDocker.class)); assertThat(result.isDisabled()).isTrue(); } @Test void whenEnabledWithoutDockerAndDockerIsAvailableTestsAreEnabled() { ConditionEvaluationResult result = new TestTestcontainersExtension(true) .evaluateExecutionCondition(extensionContext(EnabledWithoutDocker.class)); assertThat(result.isDisabled()).isFalse(); } @Test void whenEnabledWithoutDockerAndDockerIsUnavailableTestsAreEnabled() { ConditionEvaluationResult result = new TestTestcontainersExtension(false) .evaluateExecutionCondition(extensionContext(EnabledWithoutDocker.class)); assertThat(result.isDisabled()).isFalse(); } private ExtensionContext extensionContext(Class clazz) { ExtensionContext extensionContext = mock(ExtensionContext.class); when(extensionContext.getRequiredTestClass()).thenReturn(clazz); return extensionContext; } @Testcontainers(disabledWithoutDocker = true) static final class DisabledWithoutDocker {} @Testcontainers static final class EnabledWithoutDocker {} static final class TestTestcontainersExtension extends TestcontainersExtension { private final boolean dockerAvailable; private TestTestcontainersExtension(boolean dockerAvailable) { this.dockerAvailable = dockerAvailable; } boolean isDockerAvailable() { return dockerAvailable; } } } ================================================ FILE: modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/TestcontainersNestedRestartedContainerTests.java ================================================ package org.testcontainers.junit.jupiter; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import static org.assertj.core.api.Assertions.assertThat; // testClass { @Testcontainers class TestcontainersNestedRestartedContainerTests { @Container private final GenericContainer topLevelContainer = new GenericContainer<>(JUnitJupiterTestImages.HTTPD_IMAGE) .withExposedPorts(80); // }} private static String topLevelContainerId; private static String nestedContainerId; // testClass { @Test void top_level_container_should_be_running() { assertThat(topLevelContainer.isRunning()).isTrue(); // }} topLevelContainerId = topLevelContainer.getContainerId(); // testClass {{ } @Nested class NestedTestCase { @Container private final GenericContainer nestedContainer = new GenericContainer<>(JUnitJupiterTestImages.HTTPD_IMAGE) .withExposedPorts(80); @Test void both_containers_should_be_running() { // top level container is restarted for nested methods assertThat(topLevelContainer.isRunning()).isTrue(); // nested containers are only available inside their nested class assertThat(nestedContainer.isRunning()).isTrue(); // }}} if (nestedContainerId == null) { nestedContainerId = nestedContainer.getContainerId(); } else { assertThat(nestedContainer.getContainerId()).isNotEqualTo(nestedContainerId); } // testClass {{ } // } @Test void containers_should_not_be_the_same() { assertThat(nestedContainer.getContainerId()).isNotEqualTo(topLevelContainer.getContainerId()); if (nestedContainerId == null) { nestedContainerId = nestedContainer.getContainerId(); } else { assertThat(nestedContainer.getContainerId()).isNotEqualTo(nestedContainerId); } } @Test void ids_should_not_change() { assertThat(topLevelContainer.getContainerId()).isNotEqualTo(topLevelContainerId); if (nestedContainerId == null) { nestedContainerId = nestedContainer.getContainerId(); } else { assertThat(nestedContainer.getContainerId()).isNotEqualTo(nestedContainerId); } } // testClass {{{ } } // } ================================================ FILE: modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/TestcontainersNestedSharedContainerTests.java ================================================ package org.testcontainers.junit.jupiter; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import static org.assertj.core.api.Assertions.assertThat; @Testcontainers class TestcontainersNestedSharedContainerTests { @Container private static final GenericContainer TOP_LEVEL_CONTAINER = new GenericContainer<>( JUnitJupiterTestImages.HTTPD_IMAGE ) .withExposedPorts(80); private static String topLevelContainerId; @Test void top_level_container_should_be_running() { assertThat(TOP_LEVEL_CONTAINER.isRunning()).isTrue(); topLevelContainerId = TOP_LEVEL_CONTAINER.getContainerId(); } @Nested class NestedTestCase { @Test void top_level_containers_should_be_running() { assertThat(TOP_LEVEL_CONTAINER.isRunning()).isTrue(); } @Test void ids_should_not_change() { assertThat(TOP_LEVEL_CONTAINER.getContainerId()).isEqualTo(topLevelContainerId); } } } ================================================ FILE: modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/TestcontainersRestartBetweenTests.java ================================================ package org.testcontainers.junit.jupiter; import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import static org.assertj.core.api.Assertions.assertThat; @Testcontainers class TestcontainersRestartBetweenTests { @Container private GenericContainer genericContainer = new GenericContainer<>(JUnitJupiterTestImages.HTTPD_IMAGE) .withExposedPorts(80); private static String lastContainerId; @Test void first_test() { if (lastContainerId == null) { lastContainerId = genericContainer.getContainerId(); } else { assertThat(genericContainer.getContainerId()).isNotEqualTo(lastContainerId); } } @Test void second_test() { if (lastContainerId == null) { lastContainerId = genericContainer.getContainerId(); } else { assertThat(genericContainer.getContainerId()).isNotEqualTo(lastContainerId); } } } ================================================ FILE: modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/TestcontainersSharedContainerTests.java ================================================ package org.testcontainers.junit.jupiter; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import static org.assertj.core.api.Assertions.assertThat; @Testcontainers class TestcontainersSharedContainerTests { @Container private static final GenericContainer GENERIC_CONTAINER = new GenericContainer<>( JUnitJupiterTestImages.HTTPD_IMAGE ) .withExposedPorts(80); private static String lastContainerId; @BeforeAll static void doSomethingWithAContainer() { assertThat(GENERIC_CONTAINER.isRunning()).isTrue(); } @Test void first_test() { if (lastContainerId == null) { lastContainerId = GENERIC_CONTAINER.getContainerId(); } else { assertThat(GENERIC_CONTAINER.getContainerId()).isEqualTo(lastContainerId); } } @Test void second_test() { if (lastContainerId == null) { lastContainerId = GENERIC_CONTAINER.getContainerId(); } else { assertThat(GENERIC_CONTAINER.getContainerId()).isEqualTo(lastContainerId); } } } ================================================ FILE: modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/WrongAnnotationUsageTests.java ================================================ package org.testcontainers.junit.jupiter; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @Disabled @Testcontainers class WrongAnnotationUsageTests { @Container private String notStartable = "foobar"; @Test void extension_throws_exception() { assert true; } } ================================================ FILE: modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/inheritance/AbstractTestBase.java ================================================ package org.testcontainers.junit.jupiter.inheritance; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @Testcontainers abstract class AbstractTestBase { @Container static RedisContainer redisPerClass = new RedisContainer(); @Container RedisContainer redisPerTest = new RedisContainer(); } ================================================ FILE: modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/inheritance/InheritedTests.java ================================================ package org.testcontainers.junit.jupiter.inheritance; import org.junit.jupiter.api.Test; import org.testcontainers.junit.jupiter.Container; import static org.assertj.core.api.Assertions.assertThat; class InheritedTests extends AbstractTestBase { @Container private RedisContainer myRedis = new RedisContainer(); @Test void step1() { assertThat(redisPerClass.getJedis().incr("key")).isEqualTo(1); assertThat(redisPerTest.getJedis().incr("key")).isEqualTo(1); assertThat(myRedis.getJedis().incr("key")).isEqualTo(1); } @Test void step2() { assertThat(redisPerClass.getJedis().incr("key")).isEqualTo(2); assertThat(redisPerTest.getJedis().incr("key")).isEqualTo(1); assertThat(myRedis.getJedis().incr("key")).isEqualTo(1); } } ================================================ FILE: modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/inheritance/RedisContainer.java ================================================ package org.testcontainers.junit.jupiter.inheritance; import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.DockerImageName; import redis.clients.jedis.Jedis; public class RedisContainer extends GenericContainer { public RedisContainer() { super(DockerImageName.parse("redis:6-alpine")); withExposedPorts(6379); } public Jedis getJedis() { return new Jedis(getHost(), getMappedPort(6379)); } } ================================================ FILE: modules/junit-jupiter/src/test/resources/docker-compose.yml ================================================ version: '2.1' services: whoami: image: emilevauge/whoami ================================================ FILE: modules/junit-jupiter/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/k3s/build.gradle ================================================ description = "Testcontainers :: K3S" dependencies { api project(":testcontainers") // Synchronize with the jackson version, must match major and minor version shaded 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.18.4' testImplementation 'io.fabric8:kubernetes-client:7.4.0' testImplementation 'io.kubernetes:client-java:25.0.0-legacy' } ================================================ FILE: modules/k3s/src/main/java/org/testcontainers/k3s/K3sContainer.java ================================================ package org.testcontainers.k3s; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.github.dockerjava.api.command.InspectContainerResponse; import lombok.SneakyThrows; import org.apache.commons.io.IOUtils; import org.testcontainers.containers.BindMode; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; /** * Testcontainers implementation for K3S *

* Supported image: {@code rancher/k3s} */ public class K3sContainer extends GenericContainer { public static int KUBE_SECURE_PORT = 6443; public static int RANCHER_WEBHOOK_PORT = 8443; private String kubeConfigYaml; public K3sContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DockerImageName.parse("rancher/k3s")); addExposedPorts(KUBE_SECURE_PORT, RANCHER_WEBHOOK_PORT); setPrivilegedMode(true); withCreateContainerCmdModifier(it -> { it.getHostConfig().withCgroupnsMode("host"); }); addFileSystemBind("/sys/fs/cgroup", "/sys/fs/cgroup", BindMode.READ_WRITE); Map tmpFsMapping = new HashMap<>(); tmpFsMapping.put("/run", ""); tmpFsMapping.put("/var/run", ""); setTmpFsMapping(tmpFsMapping); setCommand("server", "--disable=traefik", "--tls-san=" + this.getHost()); setWaitStrategy(Wait.forLogMessage(".*Node controller sync successful.*", 1)); } @Override protected void containerIsStarted(InspectContainerResponse containerInfo) { String rawKubeConfig = copyFileFromContainer( "/etc/rancher/k3s/k3s.yaml", is -> IOUtils.toString(is, StandardCharsets.UTF_8) ); String serverUrl = "https://" + this.getHost() + ":" + this.getMappedPort(KUBE_SECURE_PORT); kubeConfigYaml = kubeConfigWithServerUrl(rawKubeConfig, serverUrl); } /** * Return the kubernetes client configuration to access k3s from the host machine. * * @return the kubeConfig yaml. */ public String getKubeConfigYaml() { return kubeConfigYaml; } /** * Generate a kubernetes client configuration for use on a docker internal network. The kubeConfig can be used by * another docker container running in the same network as the k3s container. For access from the host, use * the {@link #getKubeConfigYaml()} method instead. * * @param networkAlias a valid network alias of the k3s container. * @return the kubeConfig yaml. */ public String generateInternalKubeConfigYaml(String networkAlias) { if (this.getNetworkAliases().contains(networkAlias)) { String serverUrl = "https://" + networkAlias + ":" + KUBE_SECURE_PORT; return kubeConfigWithServerUrl(kubeConfigYaml, serverUrl); } else { throw new IllegalArgumentException(networkAlias + " is not a network alias for k3s container"); } } @SneakyThrows private String kubeConfigWithServerUrl(String kubeConfigYaml, String serverUrl) { ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); ObjectNode kubeConfigObjectNode = objectMapper.readValue(kubeConfigYaml, ObjectNode.class); JsonNode clusterNode = kubeConfigObjectNode.at("/clusters/0/cluster"); if (!clusterNode.isObject()) { throw new IllegalStateException("'/clusters/0/cluster' expected to be an object"); } ObjectNode clusterConfig = (ObjectNode) clusterNode; clusterConfig.replace("server", new TextNode(serverUrl)); kubeConfigObjectNode.set("current-context", new TextNode("default")); return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(kubeConfigObjectNode); } } ================================================ FILE: modules/k3s/src/test/java/org/testcontainers/k3s/Fabric8K3sContainerTest.java ================================================ package org.testcontainers.k3s; import io.fabric8.kubernetes.api.model.ContainerBuilder; import io.fabric8.kubernetes.api.model.ContainerPortBuilder; import io.fabric8.kubernetes.api.model.Node; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.PodBuilder; import io.fabric8.kubernetes.api.model.PodSpec; import io.fabric8.kubernetes.api.model.PodSpecBuilder; import io.fabric8.kubernetes.api.model.ProbeBuilder; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.dsl.Resource; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.testcontainers.containers.output.Slf4jLogConsumer; import org.testcontainers.utility.DockerImageName; import java.util.List; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; @Slf4j class Fabric8K3sContainerTest { @Test void shouldStartAndHaveListableNode() { try ( // starting_k3s { K3sContainer k3s = new K3sContainer(DockerImageName.parse("rancher/k3s:v1.21.3-k3s1")) .withLogConsumer(new Slf4jLogConsumer(log)) // } ) { k3s.start(); // connecting_with_fabric8 { // obtain a kubeconfig file which allows us to connect to k3s String kubeConfigYaml = k3s.getKubeConfigYaml(); // requires io.fabric8:kubernetes-client:5.11.0 or higher Config config = Config.fromKubeconfig(kubeConfigYaml); DefaultKubernetesClient client = new DefaultKubernetesClient(config); // interact with the running K3s server, e.g.: List nodes = client.nodes().list().getItems(); // } assertThat(nodes).hasSize(1); // verify that we can start a pod Pod helloworld = dummyStartablePod(); client.pods().create(helloworld); client.pods().inNamespace("default").withName("helloworld").waitUntilReady(30, TimeUnit.SECONDS); assertThat(client.pods().inNamespace("default").withName("helloworld")) .extracting(Resource::isReady) .isEqualTo(true); } } private Pod dummyStartablePod() { PodSpec podSpec = new PodSpecBuilder() .withContainers( new ContainerBuilder() .withName("helloworld") .withImage("testcontainers/helloworld:1.1.0") .withPorts(new ContainerPortBuilder().withContainerPort(8080).build()) .withReadinessProbe(new ProbeBuilder().withNewTcpSocket().withNewPort(8080).endTcpSocket().build()) .build() ) .build(); return new PodBuilder() .withNewMetadata() .withName("helloworld") .withNamespace("default") .endMetadata() .withSpec(podSpec) .build(); } } ================================================ FILE: modules/k3s/src/test/java/org/testcontainers/k3s/KubectlContainerTest.java ================================================ package org.testcontainers.k3s; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.DockerImageName; import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class KubectlContainerTest { private static final Network network = Network.SHARED; private static final K3sContainer k3s = new K3sContainer(DockerImageName.parse("rancher/k3s:v1.21.3-k3s1")) .withNetwork(network) .withNetworkAliases("k3s"); @BeforeAll static void setup() { k3s.start(); } @AfterAll static void teardown() { k3s.stop(); } @Test public void shouldExposeKubeConfigForNetworkAlias() throws Exception { String kubeConfigYaml = k3s.generateInternalKubeConfigYaml("k3s"); try ( GenericContainer kubectlContainer = new GenericContainer<>("rancher/kubectl:v1.23.3") .withNetwork(network) .withCopyToContainer(Transferable.of(kubeConfigYaml), "/.kube/config") .withCommand("get namespaces") .withStartupCheckStrategy(new OneShotStartupCheckStrategy().withTimeout(Duration.ofSeconds(30))) ) { kubectlContainer.start(); assertThat(kubectlContainer.getLogs()).contains("kube-system"); } } @Test public void shouldThrowAnExceptionForUnknownNetworkAlias() { assertThatThrownBy(() -> k3s.generateInternalKubeConfigYaml("not-set-network-alias")) .isInstanceOf(IllegalArgumentException.class); } } ================================================ FILE: modules/k3s/src/test/java/org/testcontainers/k3s/OfficialClientK3sContainerTest.java ================================================ package org.testcontainers.k3s; import io.kubernetes.client.openapi.ApiClient; import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.apis.CoreV1Api; import io.kubernetes.client.openapi.models.V1NodeList; import io.kubernetes.client.util.Config; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.testcontainers.containers.output.Slf4jLogConsumer; import org.testcontainers.utility.DockerImageName; import java.io.IOException; import java.io.StringReader; import static org.assertj.core.api.Assertions.assertThat; @Slf4j class OfficialClientK3sContainerTest { @Test void shouldStartAndHaveListableNode() throws IOException, ApiException { runK3s(DockerImageName.parse("rancher/k3s:v1.21.3-k3s1")); } @Test void shouldStartAndHaveListableNodeUsingLowerVersion() throws IOException, ApiException { runK3s(DockerImageName.parse("rancher/k3s:v1.20.15-k3s1")); } private void runK3s(DockerImageName k3sDockerImage) throws IOException, ApiException { try (K3sContainer k3s = new K3sContainer(k3sDockerImage).withLogConsumer(new Slf4jLogConsumer(log))) { k3s.start(); // connecting_with_k8sio { String kubeConfigYaml = k3s.getKubeConfigYaml(); ApiClient client = Config.fromConfig(new StringReader(kubeConfigYaml)); CoreV1Api api = new CoreV1Api(client); // interact with the running K3s server, e.g.: V1NodeList nodes = api.listNode(null, null, null, null, null, null, null, null, null, null, null); // } assertThat(nodes.getItems()).hasSize(1); } } } ================================================ FILE: modules/k3s/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/k6/build.gradle ================================================ description = "Testcontainers :: k6" dependencies { api project(':testcontainers') } ================================================ FILE: modules/k6/src/main/java/org/testcontainers/k6/K6Container.java ================================================ package org.testcontainers.k6; import org.apache.commons.io.FilenameUtils; import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; public class K6Container extends GenericContainer { /** Standard image for k6, as provided by Grafana. */ private static final DockerImageName K6_IMAGE = DockerImageName.parse("grafana/k6"); private String testScript; private List cmdOptions = new ArrayList<>(); private Map scriptVars = new HashMap<>(); /** * Creates a new container instance based upon the provided image name. */ public K6Container(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } /** * Creates a new container instance based upon the provided image. */ public K6Container(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(K6_IMAGE); } /** * Specifies the test script to be executed within the container. * @param testScript file to be copied into the container * @return the builder */ public K6Container withTestScript(MountableFile testScript) { this.testScript = "/home/k6/" + FilenameUtils.getName(testScript.getResolvedPath()); withCopyFileToContainer(testScript, this.testScript); return self(); } /** * Specifies additional command line options to be provided to the k6 command. * @param options command line options * @return the builder */ public K6Container withCmdOptions(String... options) { cmdOptions.addAll(Arrays.asList(options)); return self(); } /** * Adds a key-value pair for access within test scripts as an environment variable. * @param key unique identifier for the variable * @param value value of the variable * @return the builder */ public K6Container withScriptVar(String key, String value) { scriptVars.put(key, value); return self(); } /** * {@inheritDoc} */ @Override protected void configure() { List commandParts = new ArrayList<>(); commandParts.add("run"); commandParts.addAll(cmdOptions); for (Map.Entry entry : scriptVars.entrySet()) { commandParts.add("--env"); commandParts.add(String.format("%s=%s", entry.getKey(), entry.getValue())); } commandParts.add(testScript); setCommand(commandParts.toArray(new String[] {})); } } ================================================ FILE: modules/k6/src/test/java/org/testcontainers/k6/K6ContainerTests.java ================================================ package org.testcontainers.k6; import org.junit.jupiter.api.Test; import org.testcontainers.containers.output.WaitingConsumer; import org.testcontainers.utility.MountableFile; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; class K6ContainerTests { @Test void k6StandardTest() throws Exception { try ( // standard_k6 { K6Container container = new K6Container("grafana/k6:0.49.0") .withTestScript(MountableFile.forClasspathResource("scripts/test.js")) .withScriptVar("MY_SCRIPT_VAR", "are cool!") .withScriptVar("AN_UNUSED_VAR", "unused") .withCmdOptions("--quiet", "--no-usage-report") // } ) { container.start(); // wait { WaitingConsumer consumer = new WaitingConsumer(); container.followOutput(consumer); // Wait for test script results to be collected consumer.waitUntil( frame -> { return frame.getUtf8String().contains("iteration_duration"); }, 3, TimeUnit.SECONDS ); // } assertThat(container.getLogs()).contains("k6 tests are cool!"); } } } ================================================ FILE: modules/k6/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/k6/src/test/resources/scripts/test.js ================================================ // access_script_vars { // The most basic of k6 scripts. export default function(){ console.log(`k6 tests ${__ENV.MY_SCRIPT_VAR}`) } // } ================================================ FILE: modules/kafka/build.gradle ================================================ description = "Testcontainers :: Kafka" dependencies { api project(':testcontainers') testImplementation 'org.apache.kafka:kafka-clients:3.8.0' testImplementation 'com.google.guava:guava:23.0' testImplementation 'org.awaitility:awaitility:4.3.0' } ================================================ FILE: modules/kafka/src/main/java/org/testcontainers/containers/KafkaContainer.java ================================================ package org.testcontainers.containers; import com.github.dockerjava.api.command.InspectContainerResponse; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.ComparableVersion; import org.testcontainers.utility.DockerImageName; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; /** * Testcontainers implementation for Apache Kafka. * Zookeeper can be optionally configured. *

* Supported image: {@code confluentinc/cp-kafka} *

* Exposed ports: *

    *
  • Kafka: 9093
  • *
  • Zookeeper: 2181
  • *
* * @deprecated use {@link org.testcontainers.kafka.ConfluentKafkaContainer} or * {@link org.testcontainers.kafka.KafkaContainer} instead */ @Deprecated public class KafkaContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("confluentinc/cp-kafka"); private static final String DEFAULT_TAG = "5.4.3"; public static final int KAFKA_PORT = 9093; public static final int ZOOKEEPER_PORT = 2181; private static final String DEFAULT_INTERNAL_TOPIC_RF = "1"; private static final String STARTER_SCRIPT = "/tmp/testcontainers_start.sh"; // https://docs.confluent.io/platform/7.0.0/release-notes/index.html#ak-raft-kraft private static final String MIN_KRAFT_TAG = "7.0.0"; public static final String DEFAULT_CLUSTER_ID = "4L6g3nShT-eMCtK--X86sw"; protected String externalZookeeperConnect = null; private boolean kraftEnabled = false; private static final String PROTOCOL_PREFIX = "TC"; /** * @deprecated use {@link #KafkaContainer(DockerImageName)} instead */ @Deprecated public KafkaContainer() { this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } /** * @deprecated use {@link #KafkaContainer(DockerImageName)} instead */ @Deprecated public KafkaContainer(String confluentPlatformVersion) { this(DEFAULT_IMAGE_NAME.withTag(confluentPlatformVersion)); } public KafkaContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); } @Override KafkaContainerDef createContainerDef() { return new KafkaContainerDef(); } @Override KafkaContainerDef getContainerDef() { return (KafkaContainerDef) super.getContainerDef(); } public KafkaContainer withEmbeddedZookeeper() { if (this.kraftEnabled) { throw new IllegalStateException("Cannot configure Zookeeper when using Kraft mode"); } this.externalZookeeperConnect = null; return self(); } public KafkaContainer withExternalZookeeper(String connectString) { if (this.kraftEnabled) { throw new IllegalStateException("Cannot configure Zookeeper when using Kraft mode"); } this.externalZookeeperConnect = connectString; return self(); } public KafkaContainer withKraft() { if (this.externalZookeeperConnect != null) { throw new IllegalStateException("Cannot configure Kraft mode when Zookeeper configured"); } verifyMinKraftVersion(); this.kraftEnabled = true; return self(); } private void verifyMinKraftVersion() { String actualVersion = DockerImageName.parse(getDockerImageName()).getVersionPart(); if (new ComparableVersion(actualVersion).isLessThan(MIN_KRAFT_TAG)) { throw new IllegalArgumentException( String.format( "Provided Confluent Platform's version %s is not supported in Kraft mode (must be %s or above)", actualVersion, MIN_KRAFT_TAG ) ); } } private boolean isLessThanCP740() { String actualVersion = DockerImageName.parse(getDockerImageName()).getVersionPart(); return new ComparableVersion(actualVersion).isLessThan("7.4.0"); } public KafkaContainer withClusterId(String clusterId) { Objects.requireNonNull(clusterId, "clusterId cannot be null"); getContainerDef().withClusterId(clusterId); return self(); } public String getBootstrapServers() { return String.format("PLAINTEXT://%s:%s", getHost(), getMappedPort(KAFKA_PORT)); } @Override protected void configure() { getContainerDef().resolveListeners(); if (this.kraftEnabled) { configureKraft(); } else { configureZookeeper(); } } protected void configureKraft() { getContainerDef().withRaft(); } protected void configureZookeeper() { if (this.externalZookeeperConnect == null) { getContainerDef().withEmbeddedZookeeper(); } else { getContainerDef().withZookeeper(this.externalZookeeperConnect); } } @Override protected void containerIsStarting(InspectContainerResponse containerInfo) { super.containerIsStarting(containerInfo); List advertisedListeners = new ArrayList<>(); advertisedListeners.add(getBootstrapServers()); advertisedListeners.add(brokerAdvertisedListener(containerInfo)); List> listenersToTransform = new ArrayList<>(getContainerDef().listeners); for (int i = 0; i < listenersToTransform.size(); i++) { Supplier listenerSupplier = listenersToTransform.get(i); String protocol = String.format("%s-%d", PROTOCOL_PREFIX, i); String listener = listenerSupplier.get(); String listenerProtocol = String.format("%s://%s", protocol, listener); advertisedListeners.add(listenerProtocol); } String kafkaAdvertisedListeners = String.join(",", advertisedListeners); String command = "#!/bin/bash\n"; // exporting KAFKA_ADVERTISED_LISTENERS with the container hostname command += String.format("export KAFKA_ADVERTISED_LISTENERS=%s\n", kafkaAdvertisedListeners); if (!this.kraftEnabled || isLessThanCP740()) { // Optimization: skip the checks command += "echo '' > /etc/confluent/docker/ensure \n"; } if (this.kraftEnabled) { command += commandKraft(); } else if (this.externalZookeeperConnect == null) { command += commandZookeeper(); } // Run the original command command += "/etc/confluent/docker/run \n"; copyFileToContainer(Transferable.of(command, 0777), STARTER_SCRIPT); } protected String commandKraft() { String command = "sed -i '/KAFKA_ZOOKEEPER_CONNECT/d' /etc/confluent/docker/configure\n"; command += "echo 'kafka-storage format --ignore-formatted -t \"" + getContainerDef().getEnvVars().get("CLUSTER_ID") + "\" -c /etc/kafka/kafka.properties' >> /etc/confluent/docker/configure\n"; return command; } protected String commandZookeeper() { String command = "echo 'clientPort=" + ZOOKEEPER_PORT + "' > /tmp/zookeeper.properties\n"; command += "echo 'dataDir=/var/lib/zookeeper/data' >> /tmp/zookeeper.properties\n"; command += "echo 'dataLogDir=/var/lib/zookeeper/log' >> /tmp/zookeeper.properties\n"; command += "zookeeper-server-start /tmp/zookeeper.properties &\n"; return command; } /** * Add a {@link Supplier} that will provide a listener with format {@code host:port}. * Host will be added as a network alias. *

* The listener will be added to the list of default listeners. *

* Default listeners: *

    *
  • 0.0.0.0:9092
  • *
  • 0.0.0.0:9093
  • *
*

* Default advertised listeners: *

    *
  • {@code container.getHost():container.getMappedPort(9093)}
  • *
  • {@code container.getConfig().getHostName():9092}
  • *
* @param listenerSupplier a supplier that will provide a listener * @return this {@link KafkaContainer} instance */ public KafkaContainer withListener(Supplier listenerSupplier) { getContainerDef().withListener(listenerSupplier); return this; } protected String brokerAdvertisedListener(InspectContainerResponse containerInfo) { return String.format("BROKER://%s:%s", containerInfo.getConfig().getHostName(), "9092"); } private static class KafkaContainerDef extends ContainerDef { private final Set> listeners = new HashSet<>(); private String clusterId = DEFAULT_CLUSTER_ID; KafkaContainerDef() { // Use two listeners with different names, it will force Kafka to communicate with itself via internal // listener when KAFKA_INTER_BROKER_LISTENER_NAME is set, otherwise Kafka will try to use the advertised listener addEnvVar("KAFKA_LISTENERS", "PLAINTEXT://0.0.0.0:" + KAFKA_PORT + ",BROKER://0.0.0.0:9092"); addEnvVar("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", "BROKER:PLAINTEXT,PLAINTEXT:PLAINTEXT"); addEnvVar("KAFKA_INTER_BROKER_LISTENER_NAME", "BROKER"); addEnvVar("KAFKA_BROKER_ID", "1"); addEnvVar("KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR", DEFAULT_INTERNAL_TOPIC_RF); addEnvVar("KAFKA_OFFSETS_TOPIC_NUM_PARTITIONS", DEFAULT_INTERNAL_TOPIC_RF); addEnvVar("KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR", DEFAULT_INTERNAL_TOPIC_RF); addEnvVar("KAFKA_TRANSACTION_STATE_LOG_MIN_ISR", DEFAULT_INTERNAL_TOPIC_RF); addEnvVar("KAFKA_LOG_FLUSH_INTERVAL_MESSAGES", Long.MAX_VALUE + ""); addEnvVar("KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS", "0"); addExposedTcpPort(KAFKA_PORT); setEntrypoint("sh"); setCommand("-c", "while [ ! -f " + STARTER_SCRIPT + " ]; do sleep 0.1; done; " + STARTER_SCRIPT); setWaitStrategy(Wait.forLogMessage(".*\\[KafkaServer id=\\d+\\] started.*", 1)); } private void resolveListeners() { Set listeners = Arrays .stream(this.envVars.get("KAFKA_LISTENERS").split(",")) .collect(Collectors.toSet()); Set listenerSecurityProtocolMap = Arrays .stream(this.envVars.get("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP").split(",")) .collect(Collectors.toSet()); List> listenersToTransform = new ArrayList<>(this.listeners); for (int i = 0; i < listenersToTransform.size(); i++) { Supplier listenerSupplier = listenersToTransform.get(i); String protocol = String.format("%s-%d", PROTOCOL_PREFIX, i); String listener = listenerSupplier.get(); String listenerPort = listener.split(":")[1]; String listenerProtocol = String.format("%s://0.0.0.0:%s", protocol, listenerPort); String protocolMap = String.format("%s:PLAINTEXT", protocol); listeners.add(listenerProtocol); listenerSecurityProtocolMap.add(protocolMap); String host = listener.split(":")[0]; addNetworkAlias(host); } String kafkaListeners = String.join(",", listeners); String kafkaListenerSecurityProtocolMap = String.join(",", listenerSecurityProtocolMap); this.envVars.put("KAFKA_LISTENERS", kafkaListeners); this.envVars.put("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", kafkaListenerSecurityProtocolMap); } void withListener(Supplier listenerSupplier) { this.listeners.add(listenerSupplier); } void withEmbeddedZookeeper() { addExposedTcpPort(ZOOKEEPER_PORT); addEnvVar("KAFKA_ZOOKEEPER_CONNECT", "localhost:" + ZOOKEEPER_PORT); } void withZookeeper(String connectionString) { addEnvVar("KAFKA_ZOOKEEPER_CONNECT", connectionString); } void withClusterId(String clusterId) { this.clusterId = clusterId; } void withRaft() { this.envVars.computeIfAbsent("CLUSTER_ID", key -> clusterId); this.envVars.computeIfAbsent("KAFKA_NODE_ID", key -> getEnvVars().get("KAFKA_BROKER_ID")); addEnvVar("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", kafkaListenerSecurityProtocolMap()); addEnvVar("KAFKA_LISTENERS", kafkaListeners()); addEnvVar("KAFKA_PROCESS_ROLES", "broker,controller"); String controllerQuorumVoters = String.format("%s@localhost:9094", getEnvVars().get("KAFKA_NODE_ID")); this.envVars.computeIfAbsent("KAFKA_CONTROLLER_QUORUM_VOTERS", key -> controllerQuorumVoters); addEnvVar("KAFKA_CONTROLLER_LISTENER_NAMES", "CONTROLLER"); setWaitStrategy(Wait.forLogMessage(".*Transitioning from RECOVERY to RUNNING.*", 1)); } private String kafkaListenerSecurityProtocolMap() { String kafkaListenerSecurityProtocolMapEnvVar = getEnvVars().get("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP"); String kafkaListenerSecurityProtocolMap = String.format( "%s,CONTROLLER:PLAINTEXT", kafkaListenerSecurityProtocolMapEnvVar ); Set listenerSecurityProtocolMap = new HashSet<>( Arrays.asList(kafkaListenerSecurityProtocolMap.split(",")) ); return String.join(",", listenerSecurityProtocolMap); } private String kafkaListeners() { String kafkaListenersEnvVar = getEnvVars().get("KAFKA_LISTENERS"); String kafkaListeners = String.format("%s,CONTROLLER://0.0.0.0:9094", kafkaListenersEnvVar); Set listeners = new HashSet<>(Arrays.asList(kafkaListeners.split(","))); return String.join(",", listeners); } } } ================================================ FILE: modules/kafka/src/main/java/org/testcontainers/kafka/ConfluentKafkaContainer.java ================================================ package org.testcontainers.kafka; import com.github.dockerjava.api.command.InspectContainerResponse; import org.testcontainers.containers.GenericContainer; import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.DockerImageName; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.function.Supplier; /** * Testcontainers implementation for Confluent Kafka. *

* Supported image: {@code confluentinc/cp-kafka} *

* Exposed ports: 9092 */ public class ConfluentKafkaContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("confluentinc/cp-kafka"); private final Set listeners = new LinkedHashSet<>(); private final Set> advertisedListeners = new LinkedHashSet<>(); public ConfluentKafkaContainer(String imageName) { this(DockerImageName.parse(imageName)); } public ConfluentKafkaContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); withExposedPorts(KafkaHelper.KAFKA_PORT); withEnv(KafkaHelper.envVars()); withCommand(KafkaHelper.COMMAND); waitingFor(KafkaHelper.WAIT_STRATEGY); } @Override protected void configure() { KafkaHelper.resolveListeners(this, this.listeners); } @Override protected void containerIsStarting(InspectContainerResponse containerInfo) { String brokerAdvertisedListener = String.format( "BROKER://%s:%s", containerInfo.getConfig().getHostName(), "9093" ); List advertisedListeners = new ArrayList<>(); advertisedListeners.add("PLAINTEXT://" + getBootstrapServers()); advertisedListeners.add(brokerAdvertisedListener); advertisedListeners.addAll(KafkaHelper.resolveAdvertisedListeners(this.advertisedListeners)); String kafkaAdvertisedListeners = String.join(",", advertisedListeners); String command = "#!/bin/bash\n"; // exporting KAFKA_ADVERTISED_LISTENERS with the container hostname command += String.format("export KAFKA_ADVERTISED_LISTENERS=%s\n", kafkaAdvertisedListeners); command += "/etc/confluent/docker/run \n"; copyFileToContainer(Transferable.of(command, 0777), KafkaHelper.STARTER_SCRIPT); } /** * Add a listener in the format {@code host:port}. * Host will be included as a network alias. *

* Use it to register additional connections to the Kafka broker within the same container network. *

* The listener will be added to the list of default listeners. *

* Default listeners: *

    *
  • 0.0.0.0:9092
  • *
  • 0.0.0.0:9093
  • *
  • 0.0.0.0:9094
  • *
*

* The listener will be added to the list of default advertised listeners. *

* Default advertised listeners: *

    *
  • {@code container.getHost():container.getMappedPort(9092)}
  • *
  • {@code containerInfo.getConfig().getHostName():9093}
  • *
* @param listener a listener with format {@code host:port} * @return this {@link ConfluentKafkaContainer} instance */ public ConfluentKafkaContainer withListener(String listener) { this.listeners.add(listener); this.advertisedListeners.add(() -> listener); return this; } /** * Add a listener in the format {@code host:port} and a {@link Supplier} for the advertised listener. * Host from listener will be included as a network alias. *

* Use it to register additional connections to the Kafka broker from outside the container network *

* The listener will be added to the list of default listeners. *

* Default listeners: *

    *
  • 0.0.0.0:9092
  • *
  • 0.0.0.0:9093
  • *
  • 0.0.0.0:9094
  • *
*

* The {@link Supplier} will be added to the list of default advertised listeners. *

* Default advertised listeners: *

    *
  • {@code container.getHost():container.getMappedPort(9092)}
  • *
  • {@code containerInfo.getConfig().getHostName():9093}
  • *
* @param listener a supplier that will provide a listener * @param advertisedListener a supplier that will provide a listener * @return this {@link ConfluentKafkaContainer} instance */ public ConfluentKafkaContainer withListener(String listener, Supplier advertisedListener) { this.listeners.add(listener); this.advertisedListeners.add(advertisedListener); return this; } public String getBootstrapServers() { return String.format("%s:%s", getHost(), getMappedPort(KafkaHelper.KAFKA_PORT)); } } ================================================ FILE: modules/kafka/src/main/java/org/testcontainers/kafka/KafkaContainer.java ================================================ package org.testcontainers.kafka; import com.github.dockerjava.api.command.InspectContainerResponse; import org.testcontainers.containers.GenericContainer; import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.DockerImageName; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.function.Supplier; /** * Testcontainers implementation for Apache Kafka. *

* Supported image: {@code apache/kafka}, {@code apache/kafka-native} *

* Exposed ports: 9092 */ public class KafkaContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("apache/kafka"); private static final DockerImageName APACHE_KAFKA_NATIVE_IMAGE_NAME = DockerImageName.parse("apache/kafka-native"); private static final int KAFKA_PORT = 9092; private static final String STARTER_SCRIPT = "/tmp/testcontainers_start.sh"; private final Set listeners = new LinkedHashSet<>(); private final Set> advertisedListeners = new LinkedHashSet<>(); public KafkaContainer(String imageName) { this(DockerImageName.parse(imageName)); } public KafkaContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, APACHE_KAFKA_NATIVE_IMAGE_NAME); withExposedPorts(KAFKA_PORT); withEnv(KafkaHelper.envVars()); withCommand(KafkaHelper.COMMAND); waitingFor(KafkaHelper.WAIT_STRATEGY); } @Override protected void configure() { KafkaHelper.resolveListeners(this, this.listeners); } @Override protected void containerIsStarting(InspectContainerResponse containerInfo) { String brokerAdvertisedListener = String.format( "BROKER://%s:%s", containerInfo.getConfig().getHostName(), "9093" ); List advertisedListeners = new ArrayList<>(); advertisedListeners.add("PLAINTEXT://" + getBootstrapServers()); advertisedListeners.add(brokerAdvertisedListener); advertisedListeners.addAll(KafkaHelper.resolveAdvertisedListeners(this.advertisedListeners)); String kafkaAdvertisedListeners = String.join(",", advertisedListeners); String command = "#!/bin/bash\n"; // exporting KAFKA_ADVERTISED_LISTENERS with the container hostname command += String.format("export KAFKA_ADVERTISED_LISTENERS=%s\n", kafkaAdvertisedListeners); command += "/etc/kafka/docker/run \n"; copyFileToContainer(Transferable.of(command, 0777), STARTER_SCRIPT); } /** * Add a listener in the format {@code host:port}. * Host will be included as a network alias. *

* Use it to register additional connections to the Kafka broker within the same container network. *

* The listener will be added to the list of default listeners. *

* Default listeners: *

    *
  • 0.0.0.0:9092
  • *
  • 0.0.0.0:9093
  • *
  • 0.0.0.0:9094
  • *
*

* The listener will be added to the list of default advertised listeners. *

* Default advertised listeners: *

    *
  • {@code container.getConfig().getHostName():9092}
  • *
  • {@code container.getHost():container.getMappedPort(9093)}
  • *
* @param listener a listener with format {@code host:port} * @return this {@link KafkaContainer} instance */ public KafkaContainer withListener(String listener) { this.listeners.add(listener); this.advertisedListeners.add(() -> listener); return this; } /** * Add a listener in the format {@code host:port} and a {@link Supplier} for the advertised listener. * Host from listener will be included as a network alias. *

* Use it to register additional connections to the Kafka broker from outside the container network *

* The listener will be added to the list of default listeners. *

* Default listeners: *

    *
  • 0.0.0.0:9092
  • *
  • 0.0.0.0:9093
  • *
  • 0.0.0.0:9094
  • *
*

* The {@link Supplier} will be added to the list of default advertised listeners. *

* Default advertised listeners: *

    *
  • {@code container.getConfig().getHostName():9092}
  • *
  • {@code container.getHost():container.getMappedPort(9093)}
  • *
* @param listener a supplier that will provide a listener * @param advertisedListener a supplier that will provide a listener * @return this {@link KafkaContainer} instance */ public KafkaContainer withListener(String listener, Supplier advertisedListener) { this.listeners.add(listener); this.advertisedListeners.add(advertisedListener); return this; } public String getBootstrapServers() { return String.format("%s:%s", getHost(), getMappedPort(KAFKA_PORT)); } } ================================================ FILE: modules/kafka/src/main/java/org/testcontainers/kafka/KafkaHelper.java ================================================ package org.testcontainers.kafka; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.containers.wait.strategy.WaitStrategy; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; class KafkaHelper { private static final String DEFAULT_INTERNAL_TOPIC_RF = "1"; private static final String DEFAULT_CLUSTER_ID = "4L6g3nShT-eMCtK--X86sw"; private static final String PROTOCOL_PREFIX = "TC"; static final int KAFKA_PORT = 9092; static final String STARTER_SCRIPT = "/tmp/testcontainers_start.sh"; static final String[] COMMAND = { "sh", "-c", "while [ ! -f " + STARTER_SCRIPT + " ]; do sleep 0.1; done; " + STARTER_SCRIPT, }; static final WaitStrategy WAIT_STRATEGY = Wait.forLogMessage(".*Transitioning from RECOVERY to RUNNING.*", 1); static Map envVars() { Map envVars = new HashMap<>(); envVars.put("CLUSTER_ID", DEFAULT_CLUSTER_ID); envVars.put( "KAFKA_LISTENERS", "PLAINTEXT://0.0.0.0:" + KAFKA_PORT + ",BROKER://0.0.0.0:9093,CONTROLLER://0.0.0.0:9094" ); envVars.put( "KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", "BROKER:PLAINTEXT,PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT" ); envVars.put("KAFKA_INTER_BROKER_LISTENER_NAME", "BROKER"); envVars.put("KAFKA_PROCESS_ROLES", "broker,controller"); envVars.put("KAFKA_CONTROLLER_LISTENER_NAMES", "CONTROLLER"); envVars.put("KAFKA_NODE_ID", "1"); String controllerQuorumVoters = String.format("%s@localhost:9094", envVars.get("KAFKA_NODE_ID")); envVars.put("KAFKA_CONTROLLER_QUORUM_VOTERS", controllerQuorumVoters); envVars.put("KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR", DEFAULT_INTERNAL_TOPIC_RF); envVars.put("KAFKA_OFFSETS_TOPIC_NUM_PARTITIONS", DEFAULT_INTERNAL_TOPIC_RF); envVars.put("KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR", DEFAULT_INTERNAL_TOPIC_RF); envVars.put("KAFKA_TRANSACTION_STATE_LOG_MIN_ISR", DEFAULT_INTERNAL_TOPIC_RF); envVars.put("KAFKA_LOG_FLUSH_INTERVAL_MESSAGES", Long.MAX_VALUE + ""); envVars.put("KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS", "0"); return envVars; } static void resolveListeners(GenericContainer kafkaContainer, Set listenersSuppliers) { Set listeners = Arrays .stream(kafkaContainer.getEnvMap().get("KAFKA_LISTENERS").split(",")) .collect(Collectors.toSet()); Set listenerSecurityProtocolMap = Arrays .stream(kafkaContainer.getEnvMap().get("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP").split(",")) .collect(Collectors.toSet()); List listenersToTransform = new ArrayList<>(listenersSuppliers); for (int i = 0; i < listenersToTransform.size(); i++) { String protocol = String.format("%s-%d", PROTOCOL_PREFIX, i); String listener = listenersToTransform.get(i); String listenerHost = listener.split(":")[0]; String listenerPort = listener.split(":")[1]; String listenerProtocol = String.format("%s://%s:%s", protocol, listenerHost, listenerPort); String protocolMap = String.format("%s:PLAINTEXT", protocol); listeners.add(listenerProtocol); listenerSecurityProtocolMap.add(protocolMap); String host = listener.split(":")[0]; kafkaContainer.withNetworkAliases(host); } String kafkaListeners = String.join(",", listeners); String kafkaListenerSecurityProtocolMap = String.join(",", listenerSecurityProtocolMap); kafkaContainer.getEnvMap().put("KAFKA_LISTENERS", kafkaListeners); kafkaContainer.getEnvMap().put("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", kafkaListenerSecurityProtocolMap); } static List resolveAdvertisedListeners(Set> listenerSuppliers) { List advertisedListeners = new ArrayList<>(); List> listenersToTransform = new ArrayList<>(listenerSuppliers); for (int i = 0; i < listenersToTransform.size(); i++) { Supplier listenerSupplier = listenersToTransform.get(i); String protocol = String.format("%s-%d", PROTOCOL_PREFIX, i); String listener = listenerSupplier.get(); String listenerProtocol = String.format("%s://%s", protocol, listener); advertisedListeners.add(listenerProtocol); } return advertisedListeners; } } ================================================ FILE: modules/kafka/src/test/java/org/testcontainers/AbstractKafka.java ================================================ package org.testcontainers; import com.google.common.collect.ImmutableMap; import org.apache.kafka.clients.admin.AdminClient; import org.apache.kafka.clients.admin.AdminClientConfig; import org.apache.kafka.clients.admin.NewTopic; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecords; import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.common.config.SaslConfigs; import org.apache.kafka.common.serialization.StringDeserializer; import org.apache.kafka.common.serialization.StringSerializer; import org.awaitility.Awaitility; import java.time.Duration; import java.util.Collection; import java.util.Collections; import java.util.Map; import java.util.Properties; import java.util.UUID; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; public class AbstractKafka { private static final ImmutableMap PLAIN_PROPERTIES = ImmutableMap.of( AdminClientConfig.SECURITY_PROTOCOL_CONFIG, "SASL_PLAINTEXT", SaslConfigs.SASL_MECHANISM, "PLAIN", SaslConfigs.SASL_JAAS_CONFIG, "org.apache.kafka.common.security.plain.PlainLoginModule required username=\"admin\" password=\"admin\";" ); private static final ImmutableMap SCRAM_PROPERTIES = ImmutableMap.of( AdminClientConfig.SECURITY_PROTOCOL_CONFIG, "SASL_PLAINTEXT", SaslConfigs.SASL_MECHANISM, "SCRAM-SHA-256", SaslConfigs.SASL_JAAS_CONFIG, "org.apache.kafka.common.security.scram.ScramLoginModule required username=\"admin\" password=\"admin\";" ); protected void testKafkaFunctionality(String bootstrapServers) throws Exception { testKafkaFunctionality(bootstrapServers, false, 1, 1); } protected void testSecurePlainKafkaFunctionality(String bootstrapServers) throws Exception { testKafkaFunctionality(bootstrapServers, true, PLAIN_PROPERTIES, 1, 1); } protected void testSecureScramKafkaFunctionality(String bootstrapServers) throws Exception { testKafkaFunctionality(bootstrapServers, true, SCRAM_PROPERTIES, 1, 1); } protected void testKafkaFunctionality(String bootstrapServers, boolean authenticated, int partitions, int rf) throws Exception { testKafkaFunctionality(bootstrapServers, authenticated, Collections.emptyMap(), partitions, rf); } protected void testKafkaFunctionality( String bootstrapServers, boolean authenticated, Map authProperties, int partitions, int rf ) throws Exception { ImmutableMap adminClientDefaultProperties = ImmutableMap.of( AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers ); Properties adminClientProperties = new Properties(); adminClientProperties.putAll(adminClientDefaultProperties); ImmutableMap consumerDefaultProperties = ImmutableMap.of( ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers, ConsumerConfig.GROUP_ID_CONFIG, "tc-" + UUID.randomUUID(), ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest" ); Properties consumerProperties = new Properties(); consumerProperties.putAll(consumerDefaultProperties); ImmutableMap producerDefaultProperties = ImmutableMap.of( ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers, ProducerConfig.CLIENT_ID_CONFIG, UUID.randomUUID().toString() ); Properties producerProperties = new Properties(); producerProperties.putAll(producerDefaultProperties); if (authenticated) { adminClientProperties.putAll(authProperties); consumerProperties.putAll(authProperties); producerProperties.putAll(authProperties); } try ( AdminClient adminClient = AdminClient.create(adminClientProperties); KafkaProducer producer = new KafkaProducer<>( producerProperties, new StringSerializer(), new StringSerializer() ); KafkaConsumer consumer = new KafkaConsumer<>( consumerProperties, new StringDeserializer(), new StringDeserializer() ); ) { String topicName = "messages-" + UUID.randomUUID(); Collection topics = Collections.singletonList(new NewTopic(topicName, partitions, (short) rf)); adminClient.createTopics(topics).all().get(30, TimeUnit.SECONDS); consumer.subscribe(Collections.singletonList(topicName)); producer.send(new ProducerRecord<>(topicName, "testcontainers", "rulezzz")).get(); Awaitility .await() .atMost(Duration.ofSeconds(10)) .untilAsserted(() -> { ConsumerRecords records = consumer.poll(Duration.ofMillis(100)); assertThat(records) .hasSize(1) .extracting(ConsumerRecord::topic, ConsumerRecord::key, ConsumerRecord::value) .containsExactly(tuple(topicName, "testcontainers", "rulezzz")); }); consumer.unsubscribe(); } } protected static String getJaasConfig() { String jaasConfig = "org.apache.kafka.common.security.plain.PlainLoginModule required " + "username=\"admin\" " + "password=\"admin\" " + "user_admin=\"admin\" " + "user_test=\"secret\";"; return jaasConfig; } } ================================================ FILE: modules/kafka/src/test/java/org/testcontainers/KCatContainer.java ================================================ package org.testcontainers; import org.testcontainers.containers.GenericContainer; import org.testcontainers.images.builder.Transferable; public class KCatContainer extends GenericContainer { public KCatContainer() { super("confluentinc/cp-kcat:7.9.0"); withCreateContainerCmdModifier(cmd -> { cmd.withEntrypoint("sh"); }); withCopyToContainer(Transferable.of("Message produced by kcat"), "/data/msgs.txt"); withCommand("-c", "tail -f /dev/null"); } } ================================================ FILE: modules/kafka/src/test/java/org/testcontainers/containers/KafkaContainerTest.java ================================================ package org.testcontainers.containers; import com.google.common.collect.ImmutableMap; import lombok.SneakyThrows; import org.apache.kafka.clients.admin.AdminClient; import org.apache.kafka.clients.admin.AdminClientConfig; import org.apache.kafka.clients.admin.NewTopic; import org.apache.kafka.common.config.SaslConfigs; import org.apache.kafka.common.errors.SaslAuthenticationException; import org.apache.kafka.common.errors.TopicAuthorizationException; import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; import org.testcontainers.AbstractKafka; import org.testcontainers.Testcontainers; import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.util.Collection; import java.util.Collections; import java.util.UUID; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class KafkaContainerTest extends AbstractKafka { private static final DockerImageName KAFKA_TEST_IMAGE = DockerImageName.parse("confluentinc/cp-kafka:6.2.1"); private static final DockerImageName KAFKA_KRAFT_TEST_IMAGE = DockerImageName.parse("confluentinc/cp-kafka:7.0.1"); private static final DockerImageName ZOOKEEPER_TEST_IMAGE = DockerImageName.parse( "confluentinc/cp-zookeeper:4.0.0" ); @Test void testUsage() throws Exception { try (KafkaContainer kafka = new KafkaContainer(KAFKA_TEST_IMAGE)) { kafka.start(); testKafkaFunctionality(kafka.getBootstrapServers()); } } @Test void testUsageWithSpecificImage() throws Exception { try ( // constructorWithVersion { KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:6.2.1")) // } ) { kafka.start(); testKafkaFunctionality( // getBootstrapServers { kafka.getBootstrapServers() // } ); } } @Test void testUsageWithVersion() throws Exception { try (KafkaContainer kafka = new KafkaContainer("6.2.1")) { kafka.start(); testKafkaFunctionality(kafka.getBootstrapServers()); } } @Test void testExternalZookeeperWithExternalNetwork() throws Exception { try ( Network network = Network.newNetwork(); // withExternalZookeeper { KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:6.2.1")) .withNetwork(network) .withExternalZookeeper("zookeeper:2181"); // } GenericContainer zookeeper = new GenericContainer<>(ZOOKEEPER_TEST_IMAGE) .withNetwork(network) .withNetworkAliases("zookeeper") .withEnv("ZOOKEEPER_CLIENT_PORT", "2181"); ) { zookeeper.start(); kafka.start(); testKafkaFunctionality(kafka.getBootstrapServers()); } } @Test void testConfluentPlatformVersion7() throws Exception { try (KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.2.2"))) { kafka.start(); testKafkaFunctionality(kafka.getBootstrapServers()); } } @Test void testConfluentPlatformVersion5() throws Exception { try (KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:5.4.3"))) { kafka.start(); testKafkaFunctionality(kafka.getBootstrapServers()); } } @Test void testWithHostExposedPort() throws Exception { Testcontainers.exposeHostPorts(12345); try (KafkaContainer kafka = new KafkaContainer(KAFKA_TEST_IMAGE)) { kafka.start(); testKafkaFunctionality(kafka.getBootstrapServers()); } } @Test void testWithHostExposedPortAndExternalNetwork() throws Exception { Testcontainers.exposeHostPorts(12345); try (KafkaContainer kafka = new KafkaContainer(KAFKA_TEST_IMAGE).withNetwork(Network.newNetwork())) { kafka.start(); testKafkaFunctionality(kafka.getBootstrapServers()); } } @Test void testUsageKraftBeforeConfluentPlatformVersion74() throws Exception { try ( KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.0.1")).withKraft() ) { kafka.start(); testKafkaFunctionality(kafka.getBootstrapServers()); } } @Test void testUsageKraftAfterConfluentPlatformVersion74() throws Exception { try ( // withKraftMode { KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.4.0")).withKraft() // } ) { kafka.start(); testKafkaFunctionality(kafka.getBootstrapServers()); } } @Test void testNotSupportedKraftVersion() { try ( KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:6.2.1")).withKraft() ) {} catch (IllegalArgumentException e) { assertThat(e.getMessage()) .isEqualTo( "Provided Confluent Platform's version 6.2.1 is not supported in Kraft mode (must be 7.0.0 or above)" ); } } @Test void testKraftZookeeperMutualExclusion() { try ( KafkaContainer kafka = new KafkaContainer(KAFKA_KRAFT_TEST_IMAGE).withKraft().withExternalZookeeper("") ) {} catch (IllegalStateException e) { assertThat(e.getMessage()).isEqualTo("Cannot configure Zookeeper when using Kraft mode"); } try ( KafkaContainer kafka = new KafkaContainer(KAFKA_KRAFT_TEST_IMAGE).withExternalZookeeper("").withKraft() ) {} catch (IllegalStateException e) { assertThat(e.getMessage()).isEqualTo("Cannot configure Kraft mode when Zookeeper configured"); } try ( KafkaContainer kafka = new KafkaContainer(KAFKA_KRAFT_TEST_IMAGE).withKraft().withEmbeddedZookeeper() ) {} catch (IllegalStateException e) { assertThat(e.getMessage()).isEqualTo("Cannot configure Zookeeper when using Kraft mode"); } } @Test void testKraftPrecedenceOverEmbeddedZookeeper() throws Exception { try (KafkaContainer kafka = new KafkaContainer(KAFKA_KRAFT_TEST_IMAGE).withEmbeddedZookeeper().withKraft()) { kafka.start(); testKafkaFunctionality(kafka.getBootstrapServers()); } } @Test void testUsageWithListener() throws Exception { try ( Network network = Network.newNetwork(); KafkaContainer kafka = new KafkaContainer(KAFKA_KRAFT_TEST_IMAGE) .withListener(() -> "kafka:19092") .withNetwork(network); // createKCatContainer { GenericContainer kcat = new GenericContainer<>("confluentinc/cp-kcat:7.9.0") .withCreateContainerCmdModifier(cmd -> { cmd.withEntrypoint("sh"); }) .withCopyToContainer(Transferable.of("Message produced by kcat"), "/data/msgs.txt") .withNetwork(network) .withCommand("-c", "tail -f /dev/null") // } ) { kafka.start(); kcat.start(); // produceConsumeMessage { kcat.execInContainer("kcat", "-b", "kafka:19092", "-t", "msgs", "-P", "-l", "/data/msgs.txt"); String stdout = kcat .execInContainer("kcat", "-b", "kafka:19092", "-C", "-t", "msgs", "-c", "1") .getStdout(); // } assertThat(stdout).contains("Message produced by kcat"); } } @SneakyThrows @Test void shouldConfigureAuthenticationWithSaslUsingJaas() { try ( KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:6.2.1")) .withEnv("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", "PLAINTEXT:SASL_PLAINTEXT,BROKER:SASL_PLAINTEXT") .withEnv("KAFKA_SASL_MECHANISM_INTER_BROKER_PROTOCOL", "PLAIN") .withEnv("KAFKA_LISTENER_NAME_PLAINTEXT_SASL_ENABLED_MECHANISMS", "PLAIN") .withEnv("KAFKA_LISTENER_NAME_BROKER_SASL_ENABLED_MECHANISMS", "PLAIN") .withEnv("KAFKA_LISTENER_NAME_BROKER_PLAIN_SASL_JAAS_CONFIG", getJaasConfig()) .withEnv("KAFKA_LISTENER_NAME_PLAINTEXT_PLAIN_SASL_JAAS_CONFIG", getJaasConfig()) ) { kafka.start(); testSecurePlainKafkaFunctionality(kafka.getBootstrapServers()); } } @SneakyThrows @Test void shouldConfigureAuthenticationWithSaslScramUsingJaas() { try ( KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.7.0")) { protected String commandKraft() { String command = "sed -i '/KAFKA_ZOOKEEPER_CONNECT/d' /etc/confluent/docker/configure\n"; command += "echo 'kafka-storage format --ignore-formatted -t \"" + "$CLUSTER_ID" + "\" --add-scram SCRAM-SHA-256=[name=admin,password=admin] -c /etc/kafka/kafka.properties' >> /etc/confluent/docker/configure\n"; return command; } } .withKraft() .withEnv("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", "PLAINTEXT:SASL_PLAINTEXT,BROKER:SASL_PLAINTEXT") .withEnv("KAFKA_LISTENER_NAME_PLAINTEXT_SASL_ENABLED_MECHANISMS", "SCRAM-SHA-256") .withEnv("KAFKA_SASL_MECHANISM_INTER_BROKER_PROTOCOL", "SCRAM-SHA-256") .withEnv("KAFKA_SASL_ENABLED_MECHANISMS", "SCRAM-SHA-256") .withEnv("KAFKA_OPTS", "-Djava.security.auth.login.config=/etc/kafka/secrets/kafka_server_jaas.conf") .withCopyFileToContainer( MountableFile.forClasspathResource("kafka_server_jaas.conf"), "/etc/kafka/secrets/kafka_server_jaas.conf" ) ) { kafka.start(); testSecureScramKafkaFunctionality(kafka.getBootstrapServers()); } } @SneakyThrows @Test void enableSaslWithUnsuccessfulTopicCreation() { try ( KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:6.2.1")) .withEnv("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", "PLAINTEXT:SASL_PLAINTEXT,BROKER:SASL_PLAINTEXT") .withEnv("KAFKA_SASL_MECHANISM_INTER_BROKER_PROTOCOL", "PLAIN") .withEnv("KAFKA_LISTENER_NAME_PLAINTEXT_SASL_ENABLED_MECHANISMS", "PLAIN") .withEnv("KAFKA_LISTENER_NAME_BROKER_SASL_ENABLED_MECHANISMS", "PLAIN") .withEnv("KAFKA_LISTENER_NAME_BROKER_PLAIN_SASL_JAAS_CONFIG", getJaasConfig()) .withEnv("KAFKA_LISTENER_NAME_PLAINTEXT_PLAIN_SASL_JAAS_CONFIG", getJaasConfig()) .withEnv("KAFKA_AUTHORIZER_CLASS_NAME", "kafka.security.authorizer.AclAuthorizer") .withEnv("KAFKA_SUPER_USERS", "User:admin") ) { kafka.start(); AdminClient adminClient = AdminClient.create( ImmutableMap.of( AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers(), AdminClientConfig.SECURITY_PROTOCOL_CONFIG, "SASL_PLAINTEXT", SaslConfigs.SASL_MECHANISM, "PLAIN", SaslConfigs.SASL_JAAS_CONFIG, "org.apache.kafka.common.security.plain.PlainLoginModule required username=\"test\" password=\"secret\";" ) ); String topicName = "messages-" + UUID.randomUUID(); Collection topics = Collections.singletonList(new NewTopic(topicName, 1, (short) 1)); Awaitility .await() .untilAsserted(() -> { assertThatThrownBy(() -> adminClient.createTopics(topics).all().get(30, TimeUnit.SECONDS)) .hasCauseInstanceOf(TopicAuthorizationException.class); }); } } @SneakyThrows @Test void enableSaslAndWithAuthenticationError() { String jaasConfig = getJaasConfig(); try ( KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:6.2.1")) .withEnv("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", "PLAINTEXT:SASL_PLAINTEXT,BROKER:SASL_PLAINTEXT") .withEnv("KAFKA_SASL_MECHANISM_INTER_BROKER_PROTOCOL", "PLAIN") .withEnv("KAFKA_LISTENER_NAME_PLAINTEXT_SASL_ENABLED_MECHANISMS", "PLAIN") .withEnv("KAFKA_LISTENER_NAME_BROKER_SASL_ENABLED_MECHANISMS", "PLAIN") .withEnv("KAFKA_LISTENER_NAME_BROKER_PLAIN_SASL_JAAS_CONFIG", jaasConfig) .withEnv("KAFKA_LISTENER_NAME_PLAINTEXT_PLAIN_SASL_JAAS_CONFIG", jaasConfig) ) { kafka.start(); AdminClient adminClient = AdminClient.create( ImmutableMap.of( AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers(), AdminClientConfig.SECURITY_PROTOCOL_CONFIG, "SASL_PLAINTEXT", SaslConfigs.SASL_MECHANISM, "PLAIN", SaslConfigs.SASL_JAAS_CONFIG, "org.apache.kafka.common.security.plain.PlainLoginModule required username=\"test\" password=\"secretx\";" ) ); String topicName = "messages-" + UUID.randomUUID(); Collection topics = Collections.singletonList(new NewTopic(topicName, 1, (short) 1)); Awaitility .await() .untilAsserted(() -> { assertThatThrownBy(() -> adminClient.createTopics(topics).all().get(30, TimeUnit.SECONDS)) .hasCauseInstanceOf(SaslAuthenticationException.class); }); } } } ================================================ FILE: modules/kafka/src/test/java/org/testcontainers/kafka/CompatibleApacheKafkaImageTest.java ================================================ package org.testcontainers.kafka; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.testcontainers.AbstractKafka; public class CompatibleApacheKafkaImageTest extends AbstractKafka { public static String[] params() { return new String[] { "apache/kafka:3.8.0", "apache/kafka-native:3.8.0" }; } @ParameterizedTest @MethodSource("params") public void testUsage(String imageName) throws Exception { try (KafkaContainer kafka = new KafkaContainer(imageName)) { kafka.start(); testKafkaFunctionality(kafka.getBootstrapServers()); } } } ================================================ FILE: modules/kafka/src/test/java/org/testcontainers/kafka/ConfluentKafkaContainerTest.java ================================================ package org.testcontainers.kafka; import com.github.dockerjava.api.command.InspectContainerResponse; import lombok.SneakyThrows; import org.junit.jupiter.api.Test; import org.testcontainers.AbstractKafka; import org.testcontainers.KCatContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.SocatContainer; import org.testcontainers.utility.MountableFile; import static org.assertj.core.api.Assertions.assertThat; class ConfluentKafkaContainerTest extends AbstractKafka { @Test void testUsage() throws Exception { try ( // constructorWithVersion { ConfluentKafkaContainer kafka = new ConfluentKafkaContainer("confluentinc/cp-kafka:7.4.0") // } ) { kafka.start(); testKafkaFunctionality(kafka.getBootstrapServers()); } } @Test void testUsageWithListener() throws Exception { try ( Network network = Network.newNetwork(); // registerListener { ConfluentKafkaContainer kafka = new ConfluentKafkaContainer("confluentinc/cp-kafka:7.4.0") .withListener("kafka:19092") .withNetwork(network); // } KCatContainer kcat = new KCatContainer().withNetwork(network) ) { kafka.start(); kcat.start(); kcat.execInContainer("kcat", "-b", "kafka:19092", "-t", "msgs", "-P", "-l", "/data/msgs.txt"); String stdout = kcat .execInContainer("kcat", "-b", "kafka:19092", "-C", "-t", "msgs", "-c", "1") .getStdout(); assertThat(stdout).contains("Message produced by kcat"); } } @Test void testUsageWithListenerFromProxy() throws Exception { try ( Network network = Network.newNetwork(); // registerListenerFromProxy { SocatContainer socat = new SocatContainer().withNetwork(network).withTarget(2000, "kafka", 19092); ConfluentKafkaContainer kafka = new ConfluentKafkaContainer("confluentinc/cp-kafka:7.4.0") .withListener("kafka:19092", () -> socat.getHost() + ":" + socat.getMappedPort(2000)) .withNetwork(network) // } ) { socat.start(); kafka.start(); String bootstrapServers = String.format("%s:%s", socat.getHost(), socat.getMappedPort(2000)); testKafkaFunctionality(bootstrapServers); } } @SneakyThrows @Test void shouldConfigureAuthenticationWithSaslUsingJaas() { try ( ConfluentKafkaContainer kafka = new ConfluentKafkaContainer("confluentinc/cp-kafka:7.7.0") .withEnv( "KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", "PLAINTEXT:SASL_PLAINTEXT,BROKER:SASL_PLAINTEXT,CONTROLLER:PLAINTEXT" ) .withEnv("KAFKA_SASL_MECHANISM_INTER_BROKER_PROTOCOL", "PLAIN") .withEnv("KAFKA_LISTENER_NAME_PLAINTEXT_SASL_ENABLED_MECHANISMS", "PLAIN") .withEnv("KAFKA_LISTENER_NAME_BROKER_SASL_ENABLED_MECHANISMS", "PLAIN") .withEnv("KAFKA_LISTENER_NAME_BROKER_PLAIN_SASL_JAAS_CONFIG", getJaasConfig()) .withEnv("KAFKA_LISTENER_NAME_PLAINTEXT_PLAIN_SASL_JAAS_CONFIG", getJaasConfig()) ) { kafka.start(); testSecurePlainKafkaFunctionality(kafka.getBootstrapServers()); } } @SneakyThrows @Test void shouldConfigureAuthenticationWithSaslScramUsingJaas() { try ( ConfluentKafkaContainer kafka = new ConfluentKafkaContainer("confluentinc/cp-kafka:7.7.0") { @SneakyThrows @Override protected void containerIsStarting(InspectContainerResponse containerInfo) { String command = "echo 'kafka-storage format --ignore-formatted -t \"" + "$CLUSTER_ID" + "\" --add-scram SCRAM-SHA-256=[name=admin,password=admin] -c /etc/kafka/kafka.properties' >> /etc/confluent/docker/configure"; execInContainer("bash", "-c", command); super.containerIsStarting(containerInfo); } } .withEnv( "KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", "PLAINTEXT:SASL_PLAINTEXT,BROKER:SASL_PLAINTEXT,CONTROLLER:PLAINTEXT" ) .withEnv("KAFKA_LISTENER_NAME_PLAINTEXT_SASL_ENABLED_MECHANISMS", "SCRAM-SHA-256") .withEnv("KAFKA_SASL_MECHANISM_INTER_BROKER_PROTOCOL", "SCRAM-SHA-256") .withEnv("KAFKA_SASL_ENABLED_MECHANISMS", "SCRAM-SHA-256") .withEnv("KAFKA_OPTS", "-Djava.security.auth.login.config=/etc/kafka/secrets/kafka_server_jaas.conf") .withCopyFileToContainer( MountableFile.forClasspathResource("kafka_server_jaas.conf"), "/etc/kafka/secrets/kafka_server_jaas.conf" ) ) { kafka.start(); testSecureScramKafkaFunctionality(kafka.getBootstrapServers()); } } } ================================================ FILE: modules/kafka/src/test/java/org/testcontainers/kafka/KafkaContainerTest.java ================================================ package org.testcontainers.kafka; import org.junit.jupiter.api.Test; import org.testcontainers.AbstractKafka; import org.testcontainers.KCatContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.SocatContainer; import static org.assertj.core.api.Assertions.assertThat; class KafkaContainerTest extends AbstractKafka { @Test void testUsage() throws Exception { try ( // constructorWithVersion { KafkaContainer kafka = new KafkaContainer("apache/kafka-native:3.8.0") // } ) { kafka.start(); testKafkaFunctionality(kafka.getBootstrapServers()); } } @Test void testUsageWithListener() throws Exception { try ( Network network = Network.newNetwork(); // registerListener { KafkaContainer kafka = new KafkaContainer("apache/kafka-native:3.8.0") .withListener("kafka:19092") .withNetwork(network); // } KCatContainer kcat = new KCatContainer().withNetwork(network) ) { kafka.start(); kcat.start(); kcat.execInContainer("kcat", "-b", "kafka:19092", "-t", "msgs", "-P", "-l", "/data/msgs.txt"); String stdout = kcat .execInContainer("kcat", "-b", "kafka:19092", "-C", "-t", "msgs", "-c", "1") .getStdout(); assertThat(stdout).contains("Message produced by kcat"); } } @Test void testUsageWithListenerFromProxy() throws Exception { try ( Network network = Network.newNetwork(); SocatContainer socat = new SocatContainer().withNetwork(network).withTarget(2000, "kafka", 19092); KafkaContainer kafka = new KafkaContainer("apache/kafka-native:3.8.0") .withListener("kafka:19092", () -> socat.getHost() + ":" + socat.getMappedPort(2000)) .withNetwork(network) ) { socat.start(); kafka.start(); String bootstrapServers = String.format("%s:%s", socat.getHost(), socat.getMappedPort(2000)); testKafkaFunctionality(bootstrapServers); } } } ================================================ FILE: modules/kafka/src/test/resources/kafka_server_jaas.conf ================================================ KafkaServer { org.apache.kafka.common.security.scram.ScramLoginModule required username="admin" password="admin"; }; ================================================ FILE: modules/kafka/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/ldap/build.gradle ================================================ description = "Testcontainers :: LDAP" dependencies { api project(':testcontainers') testImplementation 'com.unboundid:unboundid-ldapsdk:7.0.4' } ================================================ FILE: modules/ldap/src/main/java/org/testcontainers/ldap/LLdapContainer.java ================================================ package org.testcontainers.ldap; import com.github.dockerjava.api.command.InspectContainerResponse; import lombok.extern.slf4j.Slf4j; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; /** * Testcontainers implementation for LLDAP. *

* Supported image: {@code lldap/lldap} *

* Exposed ports: *

    *
  • LDAP: 3890
  • *
  • UI: 17170
  • *
*/ @Slf4j public class LLdapContainer extends GenericContainer { private static final String IMAGE_VERSION = "lldap/lldap"; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(IMAGE_VERSION); private static final int LDAP_PORT = 3890; private static final int LDAPS_PORT = 6360; private static final int UI_PORT = 17170; public LLdapContainer(String image) { this(DockerImageName.parse(image)); } public LLdapContainer(DockerImageName image) { super(image); image.assertCompatibleWith(DEFAULT_IMAGE_NAME); addExposedPorts(LDAP_PORT, UI_PORT); waitingFor(Wait.forHttp("/health").forPort(UI_PORT).forStatusCode(200)); } @Override protected void containerIsStarted(InspectContainerResponse containerInfo) { log.info("LLDAP container is ready! UI available at http://{}:{}", getHost(), getMappedPort(UI_PORT)); } public LLdapContainer withBaseDn(String baseDn) { withEnv("LLDAP_LDAP_BASE_DN", baseDn); return this; } public LLdapContainer withUserPass(String userPass) { withEnv("LLDAP_LDAP_USER_PASS", userPass); return this; } public int getLdapPort() { int port = getEnvMap().getOrDefault("LLDAP_LDAPS_OPTIONS__ENABLED", "false").equals("true") ? LDAPS_PORT : LDAP_PORT; return getMappedPort(port); } public String getLdapUrl() { String protocol = getEnvMap().getOrDefault("LLDAP_LDAPS_OPTIONS__ENABLED", "false").equals("true") ? "ldaps" : "ldap"; return String.format("%s://%s:%d", protocol, getHost(), getLdapPort()); } public String getBaseDn() { return getEnvMap().getOrDefault("LLDAP_LDAP_BASE_DN", "dc=example,dc=com"); } public String getUser() { return String.format("cn=admin,ou=people,%s", getBaseDn()); } @Deprecated public String getUserPass() { return getEnvMap().getOrDefault("LLDAP_LDAP_USER_PASS", "password"); } public String getPassword() { return getEnvMap().getOrDefault("LLDAP_LDAP_USER_PASS", "password"); } } ================================================ FILE: modules/ldap/src/test/java/org/testcontainers/ldap/LLdapContainerTest.java ================================================ package org.testcontainers.ldap; import com.unboundid.ldap.sdk.BindResult; import com.unboundid.ldap.sdk.LDAPConnection; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.LDAPURL; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class LLdapContainerTest { @Test void test() throws LDAPException { try ( // container { LLdapContainer lldap = new LLdapContainer("lldap/lldap:v0.6.1-alpine") // } ) { lldap.start(); LDAPConnection connection = new LDAPConnection(lldap.getHost(), lldap.getLdapPort()); BindResult result = connection.bind(lldap.getUser(), lldap.getPassword()); assertThat(result).isNotNull(); } } @Test void testUsingLdapUrl() throws LDAPException { try (LLdapContainer lldap = new LLdapContainer("lldap/lldap:v0.6.1-alpine")) { lldap.start(); LDAPURL ldapUrl = new LDAPURL(lldap.getLdapUrl()); LDAPConnection connection = new LDAPConnection(ldapUrl.getHost(), ldapUrl.getPort()); BindResult result = connection.bind(lldap.getUser(), lldap.getPassword()); assertThat(result).isNotNull(); } } @Test void testWithCustomBaseDn() throws LDAPException { try ( LLdapContainer lldap = new LLdapContainer("lldap/lldap:v0.6.1-alpine") .withBaseDn("dc=testcontainers,dc=org") ) { lldap.start(); assertThat(lldap.getBaseDn()).isEqualTo("dc=testcontainers,dc=org"); LDAPURL ldapUrl = new LDAPURL(lldap.getLdapUrl()); LDAPConnection connection = new LDAPConnection(ldapUrl.getHost(), ldapUrl.getPort()); BindResult result = connection.bind(lldap.getUser(), lldap.getPassword()); assertThat(result).isNotNull(); } } @Test void testWithCustomUserPass() throws LDAPException { try (LLdapContainer lldap = new LLdapContainer("lldap/lldap:v0.6.1-alpine").withUserPass("adminPas$word")) { lldap.start(); LDAPURL ldapUrl = new LDAPURL(lldap.getLdapUrl()); LDAPConnection connection = new LDAPConnection(ldapUrl.getHost(), ldapUrl.getPort()); BindResult result = connection.bind(lldap.getUser(), lldap.getPassword()); assertThat(result).isNotNull(); } } } ================================================ FILE: modules/ldap/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/localstack/build.gradle ================================================ description = "Testcontainers :: Localstack" dependencies { api project(':testcontainers') testImplementation platform("software.amazon.awssdk:bom:2.40.4") testImplementation 'software.amazon.awssdk:s3' testImplementation 'software.amazon.awssdk:sqs' testImplementation 'software.amazon.awssdk:cloudwatchlogs' testImplementation 'software.amazon.awssdk:lambda' testImplementation 'software.amazon.awssdk:kms' } ================================================ FILE: modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java ================================================ package org.testcontainers.containers.localstack; import com.github.dockerjava.api.command.InspectContainerResponse; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; import lombok.extern.slf4j.Slf4j; import org.rnorth.ducttape.Preconditions; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.ComparableVersion; import org.testcontainers.utility.DockerImageName; import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; /** * Testcontainers implementation for LocalStack. *

* Supported images: {@code localstack/localstack}, {@code localstack/localstack-pro} *

* Exposed ports: 4566 * * @deprecated use {@link org.testcontainers.localstack.LocalStackContainer} instead. */ @Slf4j @Deprecated public class LocalStackContainer extends GenericContainer { static final int PORT = 4566; @Deprecated private static final String HOSTNAME_EXTERNAL_ENV_VAR = "HOSTNAME_EXTERNAL"; private static final String LOCALSTACK_HOST_ENV_VAR = "LOCALSTACK_HOST"; private final List services = new ArrayList<>(); private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("localstack/localstack"); private static final DockerImageName LOCALSTACK_PRO_IMAGE_NAME = DockerImageName.parse("localstack/localstack-pro"); private static final String DEFAULT_TAG = "0.11.2"; private static final String DEFAULT_REGION = "us-east-1"; private static final String DEFAULT_AWS_ACCESS_KEY_ID = "test"; private static final String DEFAULT_AWS_SECRET_ACCESS_KEY = "test"; private static final String STARTER_SCRIPT = "/testcontainers_start.sh"; @Deprecated public static final String VERSION = DEFAULT_TAG; /** * Whether or to assume that all APIs run on different ports (when true) or are * exposed on a single port (false). From the Localstack README: * *

Note: Starting with version 0.11.0, all APIs are exposed via a single edge * service [...] The API-specific endpoints below are still left for backward-compatibility but * may get removed in a future release - please reconfigure your client SDKs to start using the * single edge endpoint URL!
*

* Testcontainers will use the tag of the docker image to infer whether or not the used version * of Localstack supports this feature. */ private final boolean legacyMode; /** * Starting with version 0.13.0, setting services list on Localstack is not required. When false, * containers are started lazily. When true, container fails to start if services list is not provided. * * Testcontainers will use the tag of the docker image to infer whether or not the used version * of Localstack required services list. */ private final boolean servicesEnvVarRequired; private final boolean isVersion2; /** * @deprecated use {@link #LocalStackContainer(DockerImageName)} instead */ @Deprecated public LocalStackContainer() { this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } /** * @deprecated use {@link #LocalStackContainer(DockerImageName)} instead */ @Deprecated public LocalStackContainer(String version) { this(DEFAULT_IMAGE_NAME.withTag(version)); } /** * @param dockerImageName image name to use for Localstack */ public LocalStackContainer(final DockerImageName dockerImageName) { this(dockerImageName, shouldRunInLegacyMode(dockerImageName.getVersionPart())); } /** * @param dockerImageName image name to use for Localstack * @param useLegacyMode if true, each AWS service is exposed on a different port * @deprecated use {@link #LocalStackContainer(DockerImageName)} instead */ @Deprecated public LocalStackContainer(final DockerImageName dockerImageName, boolean useLegacyMode) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, LOCALSTACK_PRO_IMAGE_NAME); this.legacyMode = useLegacyMode; String version = dockerImageName.getVersionPart(); this.servicesEnvVarRequired = isServicesEnvVarRequired(version); this.isVersion2 = isVersion2(version); withFileSystemBind(DockerClientFactory.instance().getRemoteDockerUnixSocketPath(), "/var/run/docker.sock"); waitingFor(Wait.forLogMessage(".*Ready\\.\n", 1)); withCreateContainerCmdModifier(cmd -> { cmd.withEntrypoint( "sh", "-c", "while [ ! -f " + STARTER_SCRIPT + " ]; do sleep 0.1; done; " + STARTER_SCRIPT ); }); } private static boolean isVersion2(String version) { if (version.equals("latest")) { return true; } ComparableVersion comparableVersion = new ComparableVersion(version); return comparableVersion.isGreaterThanOrEqualTo("2.0.0"); } private static boolean isServicesEnvVarRequired(String version) { if (version.equals("latest")) { return false; } ComparableVersion comparableVersion = new ComparableVersion(version); if (comparableVersion.isSemanticVersion()) { return comparableVersion.isLessThan("0.13"); } log.warn("Version {} is not a semantic version, services list is required.", version); return true; } static boolean shouldRunInLegacyMode(String version) { // assume that the latest images are up-to-date // also consider images with extra packages (like latest-bigdata) and service-specific images (like s3-latest) if (version.equals("latest") || version.startsWith("latest-") || version.endsWith("-latest")) { return false; } ComparableVersion comparableVersion = new ComparableVersion(version); if (comparableVersion.isSemanticVersion()) { boolean versionRequiresLegacyMode = comparableVersion.isLessThan("0.11"); return versionRequiresLegacyMode; } log.warn("Version {} is not a semantic version, LocalStack will run in legacy mode.", version); log.warn( "Consider using \"LocalStackContainer(DockerImageName dockerImageName, boolean legacyMode)\" constructor if you want to disable legacy mode." ); return true; } @Override protected void configure() { super.configure(); if (this.servicesEnvVarRequired) { Preconditions.check("services list must not be empty", !services.isEmpty()); } if (!services.isEmpty()) { withEnv("SERVICES", services.stream().map(EnabledService::getName).collect(Collectors.joining(","))); if (this.servicesEnvVarRequired) { withEnv("EAGER_SERVICE_LOADING", "1"); } } if (this.isVersion2) { resolveHostname(LOCALSTACK_HOST_ENV_VAR); } else { resolveHostname(HOSTNAME_EXTERNAL_ENV_VAR); } exposePorts(); } @Override protected void containerIsStarting(InspectContainerResponse containerInfo) { String command = "#!/bin/bash\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); } /** * Configure the LocalStack container to include the default testcontainers labels on all spawned lambda containers * Necessary to properly clean up lambda containers even if the LocalStack container is killed before it gets the * chance. * @return the lambda container labels as a string */ private String configureServiceContainerLabels(String existingEnvFlagKey) { String internalMarkerFlags = internalMarkerLabels(); String existingFlags = getEnvMap().get(existingEnvFlagKey); if (existingFlags != null) { internalMarkerFlags = existingFlags + " " + internalMarkerFlags; } return "\"" + internalMarkerFlags + "\""; } /** * Provides a docker argument string including all default labels set on testcontainers containers (excluding reuse labels) * @return Argument string in the format `-l key1=value1 -l key2=value2` */ private String internalMarkerLabels() { return getContainerInfo() .getConfig() .getLabels() .entrySet() .stream() .filter(entry -> entry.getKey().startsWith(DockerClientFactory.TESTCONTAINERS_LABEL)) .filter(entry -> { return ( !entry.getKey().equals("org.testcontainers.hash") && !entry.getKey().equals("org.testcontainers.copied_files.hash") ); }) .map(entry -> String.format("-l %s=%s", entry.getKey(), entry.getValue())) .collect(Collectors.joining(" ")); } private void resolveHostname(String envVar) { String hostnameExternalReason; if (getEnvMap().containsKey(envVar)) { // do nothing hostnameExternalReason = "explicitly as environment variable"; } else if (getNetwork() != null && getNetworkAliases() != null && getNetworkAliases().size() >= 1) { withEnv(envVar, getNetworkAliases().get(getNetworkAliases().size() - 1)); // use the last network alias set hostnameExternalReason = "to match last network alias on container with non-default network"; } else { withEnv(envVar, getHost()); hostnameExternalReason = "to match host-routable address for container"; } logger() .info("{} environment variable set to {} ({})", envVar, getEnvMap().get(envVar), hostnameExternalReason); } private void exposePorts() { if (legacyMode) { services.stream().map(this::getServicePort).distinct().forEach(this::addExposedPort); } else { this.addExposedPort(PORT); } } public LocalStackContainer withServices(Service... services) { this.services.addAll(Arrays.asList(services)); return self(); } /** * Declare a set of simulated AWS services that should be launched by this container. * @param services one or more service names * @return this container object */ public LocalStackContainer withServices(EnabledService... services) { this.services.addAll(Arrays.asList(services)); return self(); } public URI getEndpointOverride(Service service) { return getEndpointOverride((EnabledService) service); } /** * Provides an endpoint override that is preconfigured to communicate with a given simulated service. * The provided endpoint override should be set in the AWS Java SDK v2 when building a client, e.g.: *

S3Client s3 = S3Client
             .builder()
             .endpointOverride(localstack.getEndpointOverride(LocalStackContainer.Service.S3))
             .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(
             localstack.getAccessKey(), localstack.getSecretKey()
             )))
             .region(Region.of(localstack.getRegion()))
             .build()
             
*

Please note that this method is only intended to be used for configuring AWS SDK clients * that are running on the test host. If other containers need to call this one, they should be configured * specifically to do so using a Docker network and appropriate addressing.

* * @param service the service that is to be accessed * @return an {@link URI} endpoint override */ public URI getEndpointOverride(EnabledService service) { try { final String address = getHost(); String ipAddress = address; // resolve IP address and use that as the endpoint so that path-style access is automatically used for S3 ipAddress = InetAddress.getByName(address).getHostAddress(); return new URI("http://" + ipAddress + ":" + getMappedPort(getServicePort(service))); } catch (UnknownHostException | URISyntaxException e) { throw new IllegalStateException("Cannot obtain endpoint URL", e); } } /** * Provides an endpoint to communicate with LocalStack service. * The provided endpoint should be set in the AWS Java SDK v2 when building a client, e.g.: *
S3Client s3 = S3Client
             .builder()
             .endpointOverride(localstack.getEndpoint())
             .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(
             localstack.getAccessKey(), localstack.getSecretKey()
             )))
             .region(Region.of(localstack.getRegion()))
             .build()
             
*

Please note that this method is only intended to be used for configuring AWS SDK clients * that are running on the test host. If other containers need to call this one, they should be configured * specifically to do so using a Docker network and appropriate addressing.

* * @return an {@link URI} endpoint */ public URI getEndpoint() { try { final String address = getHost(); // resolve IP address and use that as the endpoint so that path-style access is automatically used for S3 String ipAddress = InetAddress.getByName(address).getHostAddress(); return new URI("http://" + ipAddress + ":" + getMappedPort(PORT)); } catch (UnknownHostException | URISyntaxException e) { throw new IllegalStateException("Cannot obtain endpoint URL", e); } } private int getServicePort(EnabledService service) { return legacyMode ? service.getPort() : PORT; } /** * Provides a default access key that is preconfigured to communicate with a given simulated service. * AWS Access Key * The access key can be used to construct AWS SDK v2 clients: *
S3Client s3 = S3Client
             .builder()
             .endpointOverride(localstack.getEndpointOverride(LocalStackContainer.Service.S3))
             .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(
             localstack.getAccessKey(), localstack.getSecretKey()
             )))
             .region(Region.of(localstack.getRegion()))
             .build()
     
* @return a default access key */ public String getAccessKey() { return this.getEnvMap().getOrDefault("AWS_ACCESS_KEY_ID", DEFAULT_AWS_ACCESS_KEY_ID); } /** * Provides a default secret key that is preconfigured to communicate with a given simulated service. * AWS Secret Key * The secret key can be used to construct AWS SDK v2 clients: *
S3Client s3 = S3Client
             .builder()
             .endpointOverride(localstack.getEndpointOverride(LocalStackContainer.Service.S3))
             .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(
             localstack.getAccessKey(), localstack.getSecretKey()
             )))
             .region(Region.of(localstack.getRegion()))
             .build()
     
* @return a default secret key */ public String getSecretKey() { return this.getEnvMap().getOrDefault("AWS_SECRET_ACCESS_KEY", DEFAULT_AWS_SECRET_ACCESS_KEY); } /** * Provides a default region that is preconfigured to communicate with a given simulated service. * The region can be used to construct AWS SDK v2 clients: *
S3Client s3 = S3Client
             .builder()
             .endpointOverride(localstack.getEndpointOverride(LocalStackContainer.Service.S3))
             .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(
             localstack.getAccessKey(), localstack.getSecretKey()
             )))
             .region(Region.of(localstack.getRegion()))
             .build()
     
* @return a default region */ public String getRegion() { return this.getEnvMap().getOrDefault("DEFAULT_REGION", DEFAULT_REGION); } public interface EnabledService { static EnabledService named(String name) { return () -> name; } String getName(); default int getPort() { return PORT; } } @RequiredArgsConstructor @Getter @FieldDefaults(makeFinal = true) public enum Service implements EnabledService { API_GATEWAY("apigateway", 4567), EC2("ec2", 4597), KINESIS("kinesis", 4568), DYNAMODB("dynamodb", 4569), DYNAMODB_STREAMS("dynamodbstreams", 4570), // TODO: Clarify usage for ELASTICSEARCH and ELASTICSEARCH_SERVICE // ELASTICSEARCH("es", 4571), S3("s3", 4572), FIREHOSE("firehose", 4573), LAMBDA("lambda", 4574), SNS("sns", 4575), SQS("sqs", 4576), REDSHIFT("redshift", 4577), // ELASTICSEARCH_SERVICE("", 4578), SES("ses", 4579), ROUTE53("route53", 4580), CLOUDFORMATION("cloudformation", 4581), CLOUDWATCH("cloudwatch", 4582), SSM("ssm", 4583), SECRETSMANAGER("secretsmanager", 4584), STEPFUNCTIONS("stepfunctions", 4585), CLOUDWATCHLOGS("logs", 4586), STS("sts", 4592), IAM("iam", 4593), KMS("kms", 4599); String localStackName; int port; @Override public String getName() { return localStackName; } @Deprecated /* Since version 0.11, LocalStack exposes all services on a single (4566) port. */ public int getPort() { return port; } } } ================================================ FILE: modules/localstack/src/main/java/org/testcontainers/localstack/LocalStackContainer.java ================================================ package org.testcontainers.localstack; import com.github.dockerjava.api.command.InspectContainerResponse; import lombok.extern.slf4j.Slf4j; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.DockerImageName; import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; /** * Testcontainers implementation for LocalStack. *

* Supported images: {@code localstack/localstack}, {@code localstack/localstack-pro} *

* Exposed ports: 4566 */ @Slf4j public class LocalStackContainer extends GenericContainer { static final int PORT = 4566; private final List services = new ArrayList<>(); private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("localstack/localstack"); private static final DockerImageName LOCALSTACK_PRO_IMAGE_NAME = DockerImageName.parse("localstack/localstack-pro"); private static final String DEFAULT_REGION = "us-east-1"; private static final String DEFAULT_AWS_ACCESS_KEY_ID = "test"; private static final String DEFAULT_AWS_SECRET_ACCESS_KEY = "test"; private static final String STARTER_SCRIPT = "/testcontainers_start.sh"; /** * @param dockerImageName image name to use for Localstack */ public LocalStackContainer(final String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } /** * @param dockerImageName image name to use for Localstack */ public LocalStackContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, LOCALSTACK_PRO_IMAGE_NAME); withExposedPorts(PORT); withFileSystemBind(DockerClientFactory.instance().getRemoteDockerUnixSocketPath(), "/var/run/docker.sock"); waitingFor(Wait.forLogMessage(".*Ready\\.\n", 1)); withCreateContainerCmdModifier(cmd -> { cmd.withEntrypoint( "sh", "-c", "while [ ! -f " + STARTER_SCRIPT + " ]; do sleep 0.1; done; " + STARTER_SCRIPT ); }); } @Override protected void configure() { if (!services.isEmpty()) { withEnv("SERVICES", String.join(",", this.services)); } } @Override protected void containerIsStarting(InspectContainerResponse containerInfo) { String command = "#!/bin/bash\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); } /** * Configure the LocalStack container to include the default testcontainers labels on all spawned lambda containers * Necessary to properly clean up lambda containers even if the LocalStack container is killed before it gets the * chance. * @return the lambda container labels as a string */ private String configureServiceContainerLabels(String existingEnvFlagKey) { String internalMarkerFlags = internalMarkerLabels(); String existingFlags = getEnvMap().get(existingEnvFlagKey); if (existingFlags != null) { internalMarkerFlags = existingFlags + " " + internalMarkerFlags; } return "\"" + internalMarkerFlags + "\""; } /** * Provides a docker argument string including all default labels set on testcontainers containers (excluding reuse labels) * @return Argument string in the format `-l key1=value1 -l key2=value2` */ private String internalMarkerLabels() { return getContainerInfo() .getConfig() .getLabels() .entrySet() .stream() .filter(entry -> entry.getKey().startsWith(DockerClientFactory.TESTCONTAINERS_LABEL)) .filter(entry -> { return ( !entry.getKey().equals("org.testcontainers.hash") && !entry.getKey().equals("org.testcontainers.copied_files.hash") ); }) .map(entry -> String.format("-l %s=%s", entry.getKey(), entry.getValue())) .collect(Collectors.joining(" ")); } /** * Declare a set of simulated AWS services that should be launched by this container. * @param services one or more service names * @return this container object */ public LocalStackContainer withServices(String... services) { this.services.addAll(Arrays.asList(services)); return self(); } /** * Provides an endpoint to communicate with LocalStack service. * The provided endpoint should be set in the AWS Java SDK v2 when building a client, e.g.: *

S3Client s3 = S3Client
             .builder()
             .endpointOverride(localstack.getEndpoint())
             .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(
             localstack.getAccessKey(), localstack.getSecretKey()
             )))
             .region(Region.of(localstack.getRegion()))
             .build()
             
*

Please note that this method is only intended to be used for configuring AWS SDK clients * that are running on the test host. If other containers need to call this one, they should be configured * specifically to do so using a Docker network and appropriate addressing.

* * @return an {@link URI} endpoint */ public URI getEndpoint() { try { final String address = getHost(); // resolve IP address and use that as the endpoint so that path-style access is automatically used for S3 String ipAddress = InetAddress.getByName(address).getHostAddress(); return new URI("http://" + ipAddress + ":" + getMappedPort(PORT)); } catch (UnknownHostException | URISyntaxException e) { throw new IllegalStateException("Cannot obtain endpoint URL", e); } } /** * Provides a default access key that is preconfigured to communicate with a given simulated service. * AWS Access Key * The access key can be used to construct AWS SDK v2 clients: *
S3Client s3 = S3Client
             .builder()
             .endpointOverride(localstack.getEndpointOverride(LocalStackContainer.Service.S3))
             .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(
             localstack.getAccessKey(), localstack.getSecretKey()
             )))
             .region(Region.of(localstack.getRegion()))
             .build()
     
* @return a default access key */ public String getAccessKey() { return this.getEnvMap().getOrDefault("AWS_ACCESS_KEY_ID", DEFAULT_AWS_ACCESS_KEY_ID); } /** * Provides a default secret key that is preconfigured to communicate with a given simulated service. * AWS Secret Key * The secret key can be used to construct AWS SDK v2 clients: *
S3Client s3 = S3Client
             .builder()
             .endpointOverride(localstack.getEndpointOverride(LocalStackContainer.Service.S3))
             .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(
             localstack.getAccessKey(), localstack.getSecretKey()
             )))
             .region(Region.of(localstack.getRegion()))
             .build()
     
* @return a default secret key */ public String getSecretKey() { return this.getEnvMap().getOrDefault("AWS_SECRET_ACCESS_KEY", DEFAULT_AWS_SECRET_ACCESS_KEY); } /** * Provides a default region that is preconfigured to communicate with a given simulated service. * The region can be used to construct AWS SDK v2 clients: *
S3Client s3 = S3Client
             .builder()
             .endpointOverride(localstack.getEndpointOverride(LocalStackContainer.Service.S3))
             .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(
             localstack.getAccessKey(), localstack.getSecretKey()
             )))
             .region(Region.of(localstack.getRegion()))
             .build()
     
* @return a default region */ public String getRegion() { return this.getEnvMap().getOrDefault("DEFAULT_REGION", DEFAULT_REGION); } } ================================================ FILE: modules/localstack/src/test/java/org/testcontainers/containers/localstack/LegacyModeTest.java ================================================ package org.testcontainers.containers.localstack; import com.github.dockerjava.api.DockerClient; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.localstack.LocalStackContainer.Service; import org.testcontainers.images.RemoteDockerImage; import org.testcontainers.utility.DockerImageName; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; class LegacyModeTest { private static final DockerImageName LOCALSTACK_CUSTOM_TAG = DockerImageName .parse("localstack/localstack:0.12.8") .withTag("custom"); @BeforeAll static void setup() { DockerClient dockerClient = DockerClientFactory.instance().client(); dockerClient .tagImageCmd( new RemoteDockerImage(LocalstackTestImages.LOCALSTACK_0_12_IMAGE).get(), LOCALSTACK_CUSTOM_TAG.getRepository(), LOCALSTACK_CUSTOM_TAG.getVersionPart() ) .exec(); } static Stream localstackVersionWithLegacyOff() { return Stream.of( Arguments.arguments("0.12", new LocalStackContainer(LocalstackTestImages.LOCALSTACK_0_12_IMAGE)), Arguments.arguments("0.11", new LocalStackContainer(LocalstackTestImages.LOCALSTACK_0_11_IMAGE)), Arguments.arguments( "0.11 with legacy = off", new LocalStackContainer(LocalstackTestImages.LOCALSTACK_0_11_IMAGE, false) ) ); } @ParameterizedTest(name = "{0}") @MethodSource("localstackVersionWithLegacyOff") void samePortIsExposedForAllServices(String description, LocalStackContainer localstack) { localstack.withServices(Service.S3, Service.SQS); localstack.start(); try { assertThat(localstack.getExposedPorts()).as("A single port is exposed").hasSize(1); assertThat(localstack.getEndpointOverride(Service.SQS).toString()) .as("Endpoint overrides are different") .isEqualTo(localstack.getEndpointOverride(Service.S3).toString()); assertThat(localstack.getEndpointOverride(Service.SQS).toString()) .as("Endpoint configuration have different endpoints") .isEqualTo(localstack.getEndpointOverride(Service.S3).toString()); } finally { localstack.stop(); } } public static Stream localstackVersionWithLegacyOn() { return Stream.of( Arguments.arguments("0.10", new LocalStackContainer(LocalstackTestImages.LOCALSTACK_0_10_IMAGE)), Arguments.arguments("custom", new LocalStackContainer(LOCALSTACK_CUSTOM_TAG)), Arguments.arguments( "0.11 with legacy = on", new LocalStackContainer(LocalstackTestImages.LOCALSTACK_0_11_IMAGE, true) ) ); } @ParameterizedTest(name = "{0}") @MethodSource("localstackVersionWithLegacyOn") void differentPortsAreExposed(String description, LocalStackContainer localstack) { localstack.withServices(Service.S3, Service.SQS); localstack.start(); try { assertThat(localstack.getExposedPorts()).as("Multiple ports are exposed").hasSizeGreaterThan(1); assertThat(localstack.getEndpointOverride(Service.SQS).toString()) .as("Endpoint overrides are different") .isNotEqualTo(localstack.getEndpointOverride(Service.S3).toString()); assertThat(localstack.getEndpointOverride(Service.SQS).toString()) .as("Endpoint configuration have different endpoints") .isNotEqualTo(localstack.getEndpointOverride(Service.S3).toString()); } finally { localstack.stop(); } } public static Stream constructors() { return Stream.of( Arguments.arguments("latest", false), Arguments.arguments("s3-latest", false), Arguments.arguments("latest-bigdata", false), Arguments.arguments("3.4.0-bigdata", false), Arguments.arguments("3.4.0@sha256:54fcf172f6ff70909e1e26652c3bb4587282890aff0d02c20aa7695469476ac0", false), Arguments.arguments("1.4@sha256:7badf31c550f81151c485980e17542592942d7f05acc09723c5f276d41b5927d", false), Arguments.arguments("3.4.0", false), Arguments.arguments("0.12", false), Arguments.arguments("0.11", false), Arguments.arguments("sha256:8bf0d744fea26603f2b11ef7206edb38375ef954258afaeda96532a6c9c1ab8b", false), Arguments.arguments("0.10.7@sha256:45ef287e29af7285c6e4013fafea1e3567c167cd22d12282f0a5f9c7894b1c5f", true), Arguments.arguments("0.10.7", true), Arguments.arguments("0.9.6", true) ); } @ParameterizedTest @MethodSource("constructors") void testLegacyMode(String version, boolean shouldUseLegacyMode) { assertThat(LocalStackContainer.shouldRunInLegacyMode(version)).isEqualTo(shouldUseLegacyMode); } } ================================================ FILE: modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackTestImages.java ================================================ package org.testcontainers.containers.localstack; import org.testcontainers.utility.DockerImageName; public interface LocalstackTestImages { DockerImageName LOCALSTACK_IMAGE = DockerImageName.parse("localstack/localstack:4.9.2"); DockerImageName LOCALSTACK_0_10_IMAGE = LOCALSTACK_IMAGE.withTag("0.10.7"); DockerImageName LOCALSTACK_0_11_IMAGE = LOCALSTACK_IMAGE.withTag("0.11.3"); DockerImageName LOCALSTACK_0_12_IMAGE = LOCALSTACK_IMAGE.withTag("0.12.8"); DockerImageName AWS_CLI_IMAGE = DockerImageName.parse("amazon/aws-cli:2.7.27"); } ================================================ FILE: modules/localstack/src/test/java/org/testcontainers/localstack/LocalStackContainerTest.java ================================================ package org.testcontainers.localstack; import com.github.dockerjava.api.DockerClient; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.Container; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.localstack.LocalstackTestImages; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient; import software.amazon.awssdk.services.cloudwatchlogs.model.CreateLogGroupRequest; import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeLogGroupsResponse; import software.amazon.awssdk.services.kms.KmsClient; import software.amazon.awssdk.services.kms.model.CreateKeyRequest; import software.amazon.awssdk.services.kms.model.CreateKeyResponse; import software.amazon.awssdk.services.kms.model.Tag; import software.amazon.awssdk.services.lambda.LambdaClient; import software.amazon.awssdk.services.lambda.model.CreateFunctionRequest; import software.amazon.awssdk.services.lambda.model.CreateFunctionResponse; import software.amazon.awssdk.services.lambda.model.FunctionCode; import software.amazon.awssdk.services.lambda.model.GetFunctionConfigurationRequest; import software.amazon.awssdk.services.lambda.model.InvokeRequest; import software.amazon.awssdk.services.lambda.model.InvokeResponse; import software.amazon.awssdk.services.lambda.model.Runtime; import software.amazon.awssdk.services.lambda.waiters.LambdaWaiter; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.Bucket; import software.amazon.awssdk.services.s3.model.CreateBucketRequest; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.presigner.S3Presigner; import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; import software.amazon.awssdk.services.sqs.SqsClient; import software.amazon.awssdk.services.sqs.model.CreateQueueRequest; import software.amazon.awssdk.services.sqs.model.CreateQueueResponse; import software.amazon.awssdk.services.sqs.model.ReceiveMessageRequest; import software.amazon.awssdk.services.sqs.model.SendMessageRequest; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URL; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import static org.assertj.core.api.Assertions.assertThat; @Slf4j class LocalStackContainerTest { @Nested class WithoutNetwork { @Test void s3TestOverBridgeNetwork() { try ( // container { LocalStackContainer localstack = new LocalStackContainer(LocalstackTestImages.LOCALSTACK_IMAGE) .withServices("s3") // } ) { localstack.start(); // with_aws_sdk_v2 { S3Client s3 = S3Client .builder() .endpointOverride(localstack.getEndpoint()) .credentialsProvider( StaticCredentialsProvider.create( AwsBasicCredentials.create(localstack.getAccessKey(), localstack.getSecretKey()) ) ) .region(Region.of(localstack.getRegion())) .build(); // } final String bucketName = "foo"; s3.createBucket(CreateBucketRequest.builder().bucket(bucketName).build()); s3.putObject( PutObjectRequest.builder().bucket(bucketName).key("bar").build(), software.amazon.awssdk.core.sync.RequestBody.fromString("baz") ); final List buckets = s3.listBuckets().buckets(); final Optional maybeBucket = buckets .stream() .filter(b -> b.name().equals(bucketName)) .findFirst(); assertThat(maybeBucket).as("The created bucket is present").isPresent(); final Bucket bucket = maybeBucket.get(); assertThat(bucket.name()).as("The created bucket has the right name").isEqualTo(bucketName); final ListObjectsV2Response objectListing = s3.listObjectsV2( ListObjectsV2Request.builder().bucket(bucketName).build() ); assertThat(objectListing.contents()).as("The created bucket has 1 item in it").hasSize(1); final String content = s3 .getObjectAsBytes(GetObjectRequest.builder().bucket(bucketName).key("bar").build()) .asString(StandardCharsets.UTF_8); assertThat(content).as("The object can be retrieved").isEqualTo("baz"); } } @Test void sqsTestOverBridgeNetwork() { try ( LocalStackContainer localstack = new LocalStackContainer(LocalstackTestImages.LOCALSTACK_IMAGE) .withEnv("SQS_ENDPOINT_STRATEGY", "dynamic") .withServices("sqs") ) { localstack.start(); SqsClient sqs = SqsClient .builder() .endpointOverride(localstack.getEndpoint()) .credentialsProvider( StaticCredentialsProvider.create( AwsBasicCredentials.create(localstack.getAccessKey(), localstack.getSecretKey()) ) ) .region(Region.of(localstack.getRegion())) .build(); CreateQueueResponse queueResult = sqs.createQueue( CreateQueueRequest.builder().queueName("baz").build() ); String fooQueueUrl = queueResult.queueUrl(); sqs.sendMessage(SendMessageRequest.builder().queueUrl(fooQueueUrl).messageBody("test").build()); final long messageCount = sqs .receiveMessage(ReceiveMessageRequest.builder().queueUrl(fooQueueUrl).build()) .messages() .stream() .filter(message -> message.body().equals("test")) .count(); assertThat(messageCount).as("the sent message can be received").isEqualTo(1L); } } @Test void cloudWatchLogsTestOverBridgeNetwork() { try ( LocalStackContainer localstack = new LocalStackContainer(LocalstackTestImages.LOCALSTACK_IMAGE) .withServices("logs") ) { localstack.start(); CloudWatchLogsClient logs = CloudWatchLogsClient .builder() .endpointOverride(localstack.getEndpoint()) .credentialsProvider( StaticCredentialsProvider.create( AwsBasicCredentials.create(localstack.getAccessKey(), localstack.getSecretKey()) ) ) .region(Region.of(localstack.getRegion())) .build(); logs.createLogGroup(CreateLogGroupRequest.builder().logGroupName("foo").build()); DescribeLogGroupsResponse response = logs.describeLogGroups(); assertThat(response.logGroups()).as("One log group should be created").hasSize(1); assertThat(response.logGroups().get(0).logGroupName()) .as("Name of created log group is [foo]") .isEqualTo("foo"); } } @Test void kmsKeyCreationTest() { try ( LocalStackContainer localstack = new LocalStackContainer(LocalstackTestImages.LOCALSTACK_IMAGE) .withServices("kms") ) { localstack.start(); KmsClient kms = KmsClient .builder() .endpointOverride(localstack.getEndpoint()) .credentialsProvider( StaticCredentialsProvider.create( AwsBasicCredentials.create(localstack.getAccessKey(), localstack.getSecretKey()) ) ) .region(Region.of(localstack.getRegion())) .build(); String desc = "AWS CMK Description"; Tag createdByTag = Tag.builder().tagKey("CreatedBy").tagValue("StorageService").build(); CreateKeyRequest req = CreateKeyRequest.builder().description(desc).tags(createdByTag).build(); CreateKeyResponse key = kms.createKey(req); assertThat(desc) .as("AWS KMS Customer Managed Key should be created ") .isEqualTo(key.keyMetadata().description()); } } @Test void samePortIsExposedForAllServices() { try (LocalStackContainer localstack = new LocalStackContainer(LocalstackTestImages.LOCALSTACK_IMAGE)) { localstack.start(); assertThat(localstack.getExposedPorts()).as("A single port is exposed").hasSize(1); assertThat(localstack.getEndpoint().toString()) .as("Endpoint overrides are different") .isEqualTo(localstack.getEndpoint().toString()); } } } @Nested class WithNetwork { // with_network { Network network = Network.newNetwork(); LocalStackContainer localstackInDockerNetwork = new LocalStackContainer(LocalstackTestImages.LOCALSTACK_IMAGE) .withNetwork(network) .withNetworkAliases("localstack") .withServices("s3", "sqs", "logs"); // } GenericContainer awsCliInDockerNetwork = new GenericContainer<>(LocalstackTestImages.AWS_CLI_IMAGE) .withNetwork(network) .withCreateContainerCmdModifier(cmd -> cmd.withEntrypoint("tail")) .withCommand(" -f /dev/null") .withEnv("AWS_ACCESS_KEY_ID", "accesskey") .withEnv("AWS_SECRET_ACCESS_KEY", "secretkey") .withEnv("AWS_REGION", "eu-west-1"); @BeforeEach void setup() { localstackInDockerNetwork.start(); awsCliInDockerNetwork.start(); } @AfterEach void tearDown() { awsCliInDockerNetwork.stop(); localstackInDockerNetwork.stop(); } @Test void s3TestOverDockerNetwork() throws Exception { runAwsCliAgainstDockerNetworkContainer( "s3api create-bucket --bucket foo --create-bucket-configuration LocationConstraint=eu-west-1" ); runAwsCliAgainstDockerNetworkContainer("s3api list-buckets"); runAwsCliAgainstDockerNetworkContainer("s3 ls s3://foo"); } @Test void sqsTestOverDockerNetwork() throws Exception { final String queueCreationResponse = runAwsCliAgainstDockerNetworkContainer( "sqs create-queue --queue-name baz" ); runAwsCliAgainstDockerNetworkContainer( String.format( "sqs send-message --endpoint http://localstack:%d --queue-url http://sqs.eu-west-1.localhost.localstack.cloud:%d/000000000000/baz --message-body test", LocalStackContainer.PORT, LocalStackContainer.PORT ) ); final String message = runAwsCliAgainstDockerNetworkContainer( String.format( "sqs receive-message --endpoint http://localstack:%d --queue-url http://sqs.eu-west-1.localhost.localstack.cloud:%d/000000000000/baz", LocalStackContainer.PORT, LocalStackContainer.PORT ) ); assertThat(message).as("the sent message can be received").contains("\"Body\": \"test\""); } @Test void cloudWatchLogsTestOverDockerNetwork() throws Exception { runAwsCliAgainstDockerNetworkContainer("logs create-log-group --log-group-name foo"); } private String runAwsCliAgainstDockerNetworkContainer(String command) throws Exception { final String[] commandParts = String .format( "/usr/local/bin/aws --region eu-west-1 %s --endpoint-url http://localstack:%d --no-verify-ssl", command, LocalStackContainer.PORT ) .split(" "); final Container.ExecResult execResult = awsCliInDockerNetwork.execInContainer(commandParts); assertThat(execResult.getExitCode()).isEqualTo(0); final String logs = execResult.getStdout() + execResult.getStderr(); log.info(logs); return logs; } } @Nested class WithRegion { @Test void s3EndpointHasProperRegion() { try ( // with_region { LocalStackContainer localstack = new LocalStackContainer(LocalstackTestImages.LOCALSTACK_IMAGE) .withEnv("DEFAULT_REGION", "eu-west-1") .withServices("s3"); // } ) { localstack.start(); assertThat(localstack.getRegion()) .as("The endpoint configuration has right region") .isEqualTo("eu-west-1"); } } } @Nested class WithoutServices { @Test void s3ServiceStartLazily() { try (LocalStackContainer localstack = new LocalStackContainer(LocalstackTestImages.LOCALSTACK_IMAGE);) { localstack.start(); S3Client s3 = S3Client .builder() .endpointOverride(localstack.getEndpoint()) .credentialsProvider( StaticCredentialsProvider.create( AwsBasicCredentials.create(localstack.getAccessKey(), localstack.getSecretKey()) ) ) .region(Region.of(localstack.getRegion())) .build(); assertThat(s3.listBuckets().buckets()).as("S3 Service is started lazily").isEmpty(); } } } @Nested class S3SkipSignatureValidation { @Test void shouldBeAccessibleWithCredentials() throws IOException { try ( LocalStackContainer localstack = new LocalStackContainer(LocalstackTestImages.LOCALSTACK_IMAGE) .withEnv("S3_SKIP_SIGNATURE_VALIDATION", "0") ) { localstack.start(); S3Client s3 = S3Client .builder() .endpointOverride(localstack.getEndpoint()) .credentialsProvider( StaticCredentialsProvider.create( AwsBasicCredentials.create(localstack.getAccessKey(), localstack.getSecretKey()) ) ) .region(Region.of(localstack.getRegion())) .build(); final String bucketName = "foo"; s3.createBucket(CreateBucketRequest.builder().bucket(bucketName).build()); s3.putObject( PutObjectRequest.builder().bucket(bucketName).key("bar").build(), software.amazon.awssdk.core.sync.RequestBody.fromString("baz") ); final List buckets = s3.listBuckets().buckets(); final Optional maybeBucket = buckets .stream() .filter(b -> b.name().equals(bucketName)) .findFirst(); assertThat(maybeBucket).as("The created bucket is present").isPresent(); S3Presigner presigner = S3Presigner .builder() .endpointOverride(localstack.getEndpoint()) .credentialsProvider( StaticCredentialsProvider.create( AwsBasicCredentials.create(localstack.getAccessKey(), localstack.getSecretKey()) ) ) .region(Region.of(localstack.getRegion())) .build(); GetObjectPresignRequest presignRequest = GetObjectPresignRequest .builder() .signatureDuration(Duration.ofMinutes(5)) .getObjectRequest(GetObjectRequest.builder().bucket(bucketName).key("bar").build()) .build(); URL presignedUrl = presigner.presignGetObject(presignRequest).url(); assertThat(presignedUrl).as("The presigned url is valid").isNotNull(); final String content = IOUtils.toString(presignedUrl, StandardCharsets.UTF_8); assertThat(content).as("The object can be retrieved").isEqualTo("baz"); } } } @Nested class LambdaContainerLabels { private byte[] createLambdaHandlerZipFile() throws IOException { StringBuilder sb = new StringBuilder(); sb.append("def handler(event, context):\n"); sb.append(" return event"); ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(); ZipOutputStream out = new ZipOutputStream(byteOutput); ZipEntry e = new ZipEntry("handler.py"); out.putNextEntry(e); byte[] data = sb.toString().getBytes(); out.write(data, 0, data.length); out.closeEntry(); out.close(); return byteOutput.toByteArray(); } @Test void shouldLabelLambdaContainers() throws IOException { try (LocalStackContainer localstack = new LocalStackContainer(LocalstackTestImages.LOCALSTACK_IMAGE)) { localstack.start(); LambdaClient lambda = LambdaClient .builder() .endpointOverride(localstack.getEndpoint()) .credentialsProvider( StaticCredentialsProvider.create( AwsBasicCredentials.create(localstack.getAccessKey(), localstack.getSecretKey()) ) ) .region(Region.of(localstack.getRegion())) .build(); // create function byte[] handlerFile = createLambdaHandlerZipFile(); CreateFunctionRequest createFunctionRequest = CreateFunctionRequest .builder() .functionName("test-function") .runtime(Runtime.PYTHON3_11) .handler("handler.handler") .role("arn:aws:iam::000000000000:role/test-role") .code(FunctionCode.builder().zipFile(SdkBytes.fromByteArray(handlerFile)).build()) .build(); CreateFunctionResponse createFunctionResult = lambda.createFunction(createFunctionRequest); try (LambdaWaiter waiter = lambda.waiter()) { waiter.waitUntilFunctionActive( GetFunctionConfigurationRequest .builder() .functionName(createFunctionResult.functionName()) .build() ); } // invoke function once String payload = "{\"test\": \"payload\"}"; InvokeRequest invokeRequest = InvokeRequest .builder() .functionName(createFunctionResult.functionName()) .payload(SdkBytes.fromUtf8String(payload)) .build(); InvokeResponse invokeResult = lambda.invoke(invokeRequest); assertThat(invokeResult.payload().asUtf8String()) .as("Invoke result not matching expected output") .isEqualTo(payload); // assert that the spawned lambda containers has the testcontainers labels set DockerClient dockerClient = DockerClientFactory.instance().client(); Collection nameFilter = Collections.singleton(localstack.getContainerName().replace("_", "-")); com.github.dockerjava.api.model.Container lambdaContainer = dockerClient .listContainersCmd() .withNameFilter(nameFilter) .exec() .stream() .findFirst() .orElse(null); assertThat(lambdaContainer).as("Lambda container not found").isNotNull(); Map labels = lambdaContainer.getLabels(); assertThat(labels.get("org.testcontainers")).as("TestContainers label not present").isEqualTo("true"); assertThat(labels.get("org.testcontainers.sessionId")) .as("TestContainers session id not present") .isNotNull(); } } } } ================================================ FILE: modules/localstack/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/mariadb/build.gradle ================================================ description = "Testcontainers :: JDBC :: MariaDB" dependencies { api project(':testcontainers-jdbc') compileOnly project(':testcontainers-r2dbc') compileOnly 'org.mariadb:r2dbc-mariadb:1.0.3' testImplementation project(':testcontainers-jdbc-test') testImplementation 'org.mariadb.jdbc:mariadb-java-client:3.5.6' testImplementation testFixtures(project(':testcontainers-r2dbc')) testRuntimeOnly 'org.mariadb:r2dbc-mariadb:1.0.3' } ================================================ FILE: modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBContainer.java ================================================ package org.testcontainers.containers; import com.google.common.collect.Sets; import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.DockerImageName; import java.util.Set; /** * Testcontainers implementation for MariaDB. *

* Supported image: {@code mariadb} *

* Exposed ports: 3306 * * @deprecated use {@link org.testcontainers.mariadb.MariaDBContainer} instead. */ @Deprecated public class MariaDBContainer> extends JdbcDatabaseContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("mariadb"); @Deprecated public static final String DEFAULT_TAG = "10.3.6"; public static final String NAME = "mariadb"; @Deprecated public static final String IMAGE = DEFAULT_IMAGE_NAME.getUnversionedPart(); static final String DEFAULT_USER = "test"; static final String DEFAULT_PASSWORD = "test"; static final Integer MARIADB_PORT = 3306; private String databaseName = "test"; private String username = DEFAULT_USER; private String password = DEFAULT_PASSWORD; private static final String MARIADB_ROOT_USER = "root"; private static final String MY_CNF_CONFIG_OVERRIDE_PARAM_NAME = "TC_MY_CNF"; public MariaDBContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public MariaDBContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); addExposedPort(MARIADB_PORT); } @Override public Set getLivenessCheckPortNumbers() { return Sets.newHashSet(MARIADB_PORT); } @Override protected void configure() { optionallyMapResourceParameterAsVolume( MY_CNF_CONFIG_OVERRIDE_PARAM_NAME, "/etc/mysql/conf.d", "mariadb-default-conf", Transferable.DEFAULT_DIR_MODE ); addEnv("MYSQL_DATABASE", databaseName); if (!MARIADB_ROOT_USER.equalsIgnoreCase(this.username)) { addEnv("MYSQL_USER", username); } if (password != null && !password.isEmpty()) { addEnv("MYSQL_PASSWORD", password); addEnv("MYSQL_ROOT_PASSWORD", password); } else if (MARIADB_ROOT_USER.equalsIgnoreCase(username)) { addEnv("MYSQL_ALLOW_EMPTY_PASSWORD", "yes"); } else { throw new ContainerLaunchException("Empty password can be used only with the root user"); } setStartupAttempts(3); } @Override public String getDriverClassName() { return "org.mariadb.jdbc.Driver"; } @Override public String getJdbcUrl() { String additionalUrlParams = constructUrlParameters("?", "&"); return ( "jdbc:mariadb://" + getHost() + ":" + getMappedPort(MARIADB_PORT) + "/" + databaseName + additionalUrlParams ); } @Override public String getDatabaseName() { return databaseName; } @Override public String getUsername() { return username; } @Override public String getPassword() { return password; } @Override public String getTestQueryString() { return "SELECT 1"; } public SELF withConfigurationOverride(String s) { parameters.put(MY_CNF_CONFIG_OVERRIDE_PARAM_NAME, s); return self(); } @Override public SELF withDatabaseName(final String databaseName) { this.databaseName = databaseName; return self(); } @Override public SELF withUsername(final String username) { this.username = username; return self(); } @Override public SELF withPassword(final String password) { this.password = password; return self(); } } ================================================ FILE: modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBContainerProvider.java ================================================ package org.testcontainers.containers; import org.testcontainers.jdbc.ConnectionUrl; import org.testcontainers.utility.DockerImageName; /** * Factory for MariaDB org.testcontainers.containers. */ public class MariaDBContainerProvider extends JdbcDatabaseContainerProvider { private static final String USER_PARAM = "user"; private static final String PASSWORD_PARAM = "password"; @Override public boolean supports(String databaseType) { return databaseType.equals(MariaDBContainer.NAME); } @Override public JdbcDatabaseContainer newInstance() { return newInstance(MariaDBContainer.DEFAULT_TAG); } @Override public JdbcDatabaseContainer newInstance(String tag) { return new MariaDBContainer(DockerImageName.parse(MariaDBContainer.IMAGE).withTag(tag)); } @Override public JdbcDatabaseContainer newInstance(ConnectionUrl connectionUrl) { return newInstanceFromConnectionUrl(connectionUrl, USER_PARAM, PASSWORD_PARAM); } } ================================================ FILE: modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainer.java ================================================ package org.testcontainers.containers; import io.r2dbc.spi.ConnectionFactoryOptions; import lombok.RequiredArgsConstructor; import lombok.experimental.Delegate; import org.testcontainers.lifecycle.Startable; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; @RequiredArgsConstructor public class MariaDBR2DBCDatabaseContainer implements R2DBCDatabaseContainer { @Delegate(types = Startable.class) private final MariaDBContainer container; public static ConnectionFactoryOptions getOptions(MariaDBContainer container) { ConnectionFactoryOptions options = ConnectionFactoryOptions .builder() .option(ConnectionFactoryOptions.DRIVER, MariaDBR2DBCDatabaseContainerProvider.DRIVER) .build(); return new MariaDBR2DBCDatabaseContainer(container).configure(options); } @Override public ConnectionFactoryOptions configure(ConnectionFactoryOptions options) { return options .mutate() .option(ConnectionFactoryOptions.HOST, container.getHost()) .option(ConnectionFactoryOptions.PORT, container.getMappedPort(MariaDBContainer.MARIADB_PORT)) .option(ConnectionFactoryOptions.DATABASE, container.getDatabaseName()) .option(ConnectionFactoryOptions.USER, container.getUsername()) .option(ConnectionFactoryOptions.PASSWORD, container.getPassword()) .build(); } } ================================================ FILE: modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerProvider.java ================================================ package org.testcontainers.containers; import io.r2dbc.spi.ConnectionFactoryMetadata; import io.r2dbc.spi.ConnectionFactoryOptions; import org.mariadb.r2dbc.MariadbConnectionFactoryProvider; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; import org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider; import javax.annotation.Nullable; public class MariaDBR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider { static final String DRIVER = MariadbConnectionFactoryProvider.MARIADB_DRIVER; @Override public boolean supports(ConnectionFactoryOptions options) { return DRIVER.equals(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)); } @Override public R2DBCDatabaseContainer createContainer(ConnectionFactoryOptions options) { String image = MariaDBContainer.IMAGE + ":" + options.getRequiredValue(IMAGE_TAG_OPTION); MariaDBContainer container = new MariaDBContainer<>(image) .withDatabaseName((String) options.getRequiredValue(ConnectionFactoryOptions.DATABASE)); if (Boolean.TRUE.equals(options.getValue(REUSABLE_OPTION))) { container.withReuse(true); } return new MariaDBR2DBCDatabaseContainer(container); } @Nullable @Override public ConnectionFactoryMetadata getMetadata(ConnectionFactoryOptions options) { ConnectionFactoryOptions.Builder builder = options.mutate(); if (!options.hasOption(ConnectionFactoryOptions.USER)) { builder.option(ConnectionFactoryOptions.USER, MariaDBContainer.DEFAULT_USER); } if (!options.hasOption(ConnectionFactoryOptions.PASSWORD)) { builder.option(ConnectionFactoryOptions.PASSWORD, MariaDBContainer.DEFAULT_PASSWORD); } return R2DBCDatabaseContainerProvider.super.getMetadata(builder.build()); } } ================================================ FILE: modules/mariadb/src/main/java/org/testcontainers/mariadb/MariaDBContainer.java ================================================ package org.testcontainers.mariadb; import com.google.common.collect.Sets; import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.DockerImageName; import java.util.Set; /** * Testcontainers implementation for MariaDB. *

* Supported image: {@code mariadb} *

* Exposed ports: 3306 */ public class MariaDBContainer extends JdbcDatabaseContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("mariadb"); public static final String NAME = "mariadb"; static final String DEFAULT_USER = "test"; static final String DEFAULT_PASSWORD = "test"; static final Integer MARIADB_PORT = 3306; private String databaseName = "test"; private String username = DEFAULT_USER; private String password = DEFAULT_PASSWORD; private static final String MARIADB_ROOT_USER = "root"; private static final String MY_CNF_CONFIG_OVERRIDE_PARAM_NAME = "TC_MY_CNF"; public MariaDBContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public MariaDBContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); addExposedPort(MARIADB_PORT); } @Override public Set getLivenessCheckPortNumbers() { return Sets.newHashSet(MARIADB_PORT); } @Override protected void configure() { optionallyMapResourceParameterAsVolume( MY_CNF_CONFIG_OVERRIDE_PARAM_NAME, "/etc/mysql/conf.d", null, Transferable.DEFAULT_DIR_MODE ); addEnv("MYSQL_DATABASE", databaseName); if (!MARIADB_ROOT_USER.equalsIgnoreCase(this.username)) { addEnv("MYSQL_USER", username); } if (password != null && !password.isEmpty()) { addEnv("MYSQL_PASSWORD", password); addEnv("MYSQL_ROOT_PASSWORD", password); } else if (MARIADB_ROOT_USER.equalsIgnoreCase(username)) { addEnv("MYSQL_ALLOW_EMPTY_PASSWORD", "yes"); } else { throw new ContainerLaunchException("Empty password can be used only with the root user"); } setStartupAttempts(3); } @Override public String getDriverClassName() { return "org.mariadb.jdbc.Driver"; } @Override public String getJdbcUrl() { String additionalUrlParams = constructUrlParameters("?", "&"); return ( "jdbc:mariadb://" + getHost() + ":" + getMappedPort(MARIADB_PORT) + "/" + databaseName + additionalUrlParams ); } @Override public String getDatabaseName() { return databaseName; } @Override public String getUsername() { return username; } @Override public String getPassword() { return password; } @Override public String getTestQueryString() { return "SELECT 1"; } public MariaDBContainer withConfigurationOverride(String s) { parameters.put(MY_CNF_CONFIG_OVERRIDE_PARAM_NAME, s); return self(); } @Override public MariaDBContainer withDatabaseName(final String databaseName) { this.databaseName = databaseName; return self(); } @Override public MariaDBContainer withUsername(final String username) { this.username = username; return self(); } @Override public MariaDBContainer withPassword(final String password) { this.password = password; return self(); } } ================================================ FILE: modules/mariadb/src/main/java/org/testcontainers/mariadb/MariaDBR2DBCDatabaseContainer.java ================================================ package org.testcontainers.mariadb; import io.r2dbc.spi.ConnectionFactoryOptions; import org.mariadb.r2dbc.MariadbConnectionFactoryProvider; import org.testcontainers.lifecycle.Startable; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; import java.util.Set; public class MariaDBR2DBCDatabaseContainer implements R2DBCDatabaseContainer { private final MariaDBContainer container; public MariaDBR2DBCDatabaseContainer(MariaDBContainer container) { this.container = container; } public static ConnectionFactoryOptions getOptions(MariaDBContainer container) { ConnectionFactoryOptions options = ConnectionFactoryOptions .builder() .option(ConnectionFactoryOptions.DRIVER, MariadbConnectionFactoryProvider.MARIADB_DRIVER) .build(); return new MariaDBR2DBCDatabaseContainer(container).configure(options); } @Override public ConnectionFactoryOptions configure(ConnectionFactoryOptions options) { return options .mutate() .option(ConnectionFactoryOptions.HOST, container.getHost()) .option(ConnectionFactoryOptions.PORT, container.getMappedPort(MariaDBContainer.MARIADB_PORT)) .option(ConnectionFactoryOptions.DATABASE, container.getDatabaseName()) .option(ConnectionFactoryOptions.USER, container.getUsername()) .option(ConnectionFactoryOptions.PASSWORD, container.getPassword()) .build(); } @Override public Set getDependencies() { return this.container.getDependencies(); } @Override public void start() { this.container.start(); } @Override public void stop() { this.container.stop(); } @Override public void close() { this.container.close(); } } ================================================ FILE: modules/mariadb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider ================================================ org.testcontainers.containers.MariaDBContainerProvider ================================================ FILE: modules/mariadb/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider ================================================ org.testcontainers.containers.MariaDBR2DBCDatabaseContainerProvider ================================================ FILE: modules/mariadb/src/main/resources/mariadb-default-conf/my.cnf ================================================ [mysqld] port = 3306 #socket = /tmp/mysql.sock skip-external-locking key_buffer_size = 16K max_allowed_packet = 1M table_open_cache = 4 sort_buffer_size = 64K read_buffer_size = 256K read_rnd_buffer_size = 256K net_buffer_length = 2K thread_stack = 512K skip-host-cache skip-name-resolve # Don't listen on a TCP/IP port at all. This can be a security enhancement, # if all processes that need to connect to mysqld run on the same host. # All interaction with mysqld must be made via Unix sockets or named pipes. # Note that using this option without enabling named pipes on Windows # (using the "enable-named-pipe" option) will render mysqld useless! # #skip-networking #server-id = 1 # Uncomment the following if you want to log updates #log-bin=mysql-bin # binary logging format - mixed recommended #binlog_format=mixed # Causes updates to non-transactional engines using statement format to be # written directly to binary log. Before using this option make sure that # there are no dependencies between transactional and non-transactional # tables such as in the statement INSERT INTO t_myisam SELECT * FROM # t_innodb; otherwise, slaves may diverge from the master. #binlog_direct_non_transactional_updates=TRUE # Uncomment the following if you are using InnoDB tables innodb_data_file_path = ibdata1:10M:autoextend # You can set .._buffer_pool_size up to 50 - 80 % # of RAM but beware of setting memory usage too high innodb_buffer_pool_size = 16M #innodb_additional_mem_pool_size = 2M # Set .._log_file_size to 25 % of buffer pool size innodb_log_file_size = 5M innodb_log_buffer_size = 8M innodb_flush_log_at_trx_commit = 1 innodb_lock_wait_timeout = 50 ================================================ FILE: modules/mariadb/src/test/java/org/testcontainers/MariaDBTestImages.java ================================================ package org.testcontainers; import org.testcontainers.utility.DockerImageName; public interface MariaDBTestImages { DockerImageName MARIADB_IMAGE = DockerImageName.parse("mariadb:10.3.39"); } ================================================ FILE: modules/mariadb/src/test/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerTest.java ================================================ package org.testcontainers.containers; import io.r2dbc.spi.ConnectionFactoryOptions; import org.testcontainers.r2dbc.AbstractR2DBCDatabaseContainerTest; import org.testcontainers.utility.DockerImageName; public class MariaDBR2DBCDatabaseContainerTest extends AbstractR2DBCDatabaseContainerTest> { @Override protected ConnectionFactoryOptions getOptions(MariaDBContainer container) { return MariaDBR2DBCDatabaseContainer.getOptions(container); } @Override protected String createR2DBCUrl() { return "r2dbc:tc:mariadb:///db?TC_IMAGE_TAG=10.3.39"; } @Override protected MariaDBContainer createContainer() { return new MariaDBContainer<>(DockerImageName.parse("mariadb:10.3.39")); } } ================================================ FILE: modules/mariadb/src/test/java/org/testcontainers/jdbc/mariadb/MariaDBJDBCDriverTest.java ================================================ package org.testcontainers.jdbc.mariadb; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; class MariaDBJDBCDriverTest extends AbstractJDBCDriverTest { public static Iterable data() { return Arrays.asList( new Object[][] { { "jdbc:tc:mariadb://hostname/databasename", EnumSet.noneOf(Options.class) }, { "jdbc:tc:mariadb://hostname/databasename?user=someuser&TC_INITSCRIPT=somepath/init_mariadb.sql", EnumSet.of(Options.ScriptedSchema, Options.JDBCParams), }, { "jdbc:tc:mariadb:10.3.39://hostname/databasename", EnumSet.noneOf(Options.class) }, { "jdbc:tc:mariadb:10.3.39://hostname/databasename?TC_INITSCRIPT=somepath/init_unicode_mariadb.sql&useUnicode=yes&characterEncoding=utf8", EnumSet.of(Options.CharacterSet), }, { "jdbc:tc:mariadb:10.3.39://hostname/databasename?user=someuser&TC_INITSCRIPT=somepath/init_mariadb.sql", EnumSet.of(Options.ScriptedSchema, Options.JDBCParams), }, { "jdbc:tc:mariadb:10.3.39://hostname/databasename?user=someuser&TC_INITFUNCTION=org.testcontainers.jdbc.AbstractJDBCDriverTest::sampleInitFunction", EnumSet.of(Options.ScriptedSchema, Options.JDBCParams), }, { "jdbc:tc:mariadb:10.3.39://hostname/databasename?user=someuser&password=somepwd&TC_INITSCRIPT=somepath/init_mariadb.sql", EnumSet.of(Options.ScriptedSchema, Options.JDBCParams), }, { "jdbc:tc:mariadb:10.3.39://hostname/databasename?user=someuser&password=somepwd&TC_INITFUNCTION=org.testcontainers.jdbc.AbstractJDBCDriverTest::sampleInitFunction", EnumSet.of(Options.ScriptedSchema, Options.JDBCParams), }, { "jdbc:tc:mariadb:10.3.39://hostname/databasename?TC_MY_CNF=somepath/mariadb_conf_override", EnumSet.of(Options.CustomIniFile), }, } ); } } ================================================ FILE: modules/mariadb/src/test/java/org/testcontainers/mariadb/MariaDBContainerTest.java ================================================ package org.testcontainers.mariadb; import org.apache.commons.lang3.SystemUtils; import org.junit.jupiter.api.Test; import org.testcontainers.MariaDBTestImages; import org.testcontainers.db.AbstractContainerDatabaseTest; import java.io.File; import java.net.URL; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.attribute.PosixFilePermission; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assumptions.assumeThat; class MariaDBContainerTest extends AbstractContainerDatabaseTest { @Test void testSimple() throws SQLException { try ( // container { MariaDBContainer mariadb = new MariaDBContainer("mariadb:10.3.39") // } ) { mariadb.start(); ResultSet resultSet = performQuery(mariadb, "SELECT 1"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); } } @Test void testSpecificVersion() throws SQLException { try ( MariaDBContainer mariadbOldVersion = new MariaDBContainer( MariaDBTestImages.MARIADB_IMAGE.withTag("10.3.39") ) ) { mariadbOldVersion.start(); ResultSet resultSet = performQuery(mariadbOldVersion, "SELECT VERSION()"); String resultSetString = resultSet.getString(1); assertThat(resultSetString) .as("The database version can be set using a container rule parameter") .startsWith("10.3.39"); } } @Test void testMariaDBWithCustomIniFile() throws SQLException { assumeThat(SystemUtils.IS_OS_WINDOWS).isFalse(); try ( MariaDBContainer mariadbCustomConfig = new MariaDBContainer( MariaDBTestImages.MARIADB_IMAGE.withTag("10.3.39") ) .withConfigurationOverride("somepath/mariadb_conf_override") ) { mariadbCustomConfig.start(); assertThatCustomIniFileWasUsed(mariadbCustomConfig); } } @Test void testMariaDBWithCommandOverride() throws SQLException { try ( MariaDBContainer mariadbCustomConfig = new MariaDBContainer(MariaDBTestImages.MARIADB_IMAGE) .withCommand("mysqld --auto_increment_increment=10") ) { mariadbCustomConfig.start(); ResultSet resultSet = performQuery(mariadbCustomConfig, "show variables like 'auto_increment_increment'"); String result = resultSet.getString("Value"); assertThat(result).as("Auto increment increment should be overridden by command line").isEqualTo("10"); } } @Test void testWithAdditionalUrlParamInJdbcUrl() { MariaDBContainer mariaDBContainer = new MariaDBContainer(MariaDBTestImages.MARIADB_IMAGE) .withUrlParam("connectTimeout", "40000") .withUrlParam("rewriteBatchedStatements", "true"); try { mariaDBContainer.start(); String jdbcUrl = mariaDBContainer.getJdbcUrl(); assertThat(jdbcUrl).contains("?"); assertThat(jdbcUrl).contains("&"); assertThat(jdbcUrl).contains("rewriteBatchedStatements=true"); assertThat(jdbcUrl).contains("connectTimeout=40000"); } finally { mariaDBContainer.stop(); } } @Test void testWithOnlyUserReadableCustomIniFile() throws Exception { assumeThat(FileSystems.getDefault().supportedFileAttributeViews().contains("posix")).isTrue(); try ( MariaDBContainer mariadbCustomConfig = new MariaDBContainer( MariaDBTestImages.MARIADB_IMAGE.withTag("10.3.39") ) .withConfigurationOverride("somepath/mariadb_conf_override") ) { URL resource = this.getClass().getClassLoader().getResource("somepath/mariadb_conf_override"); File file = new File(resource.toURI()); assertThat(file.isDirectory()).isTrue(); Set permissions = new HashSet<>( Arrays.asList( PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE ) ); Files.setPosixFilePermissions(file.toPath(), permissions); mariadbCustomConfig.start(); assertThatCustomIniFileWasUsed(mariadbCustomConfig); } } @Test void testEmptyPasswordWithRootUser() throws SQLException { try (MariaDBContainer mysql = new MariaDBContainer("mariadb:11.2.4").withUsername("root")) { mysql.start(); ResultSet resultSet = performQuery(mysql, "SELECT 1"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).isEqualTo(1); } } private void assertThatCustomIniFileWasUsed(MariaDBContainer mariadb) throws SQLException { try (ResultSet resultSet = performQuery(mariadb, "SELECT @@GLOBAL.innodb_max_undo_log_size")) { long result = resultSet.getLong(1); assertThat(result) .as("The InnoDB max undo log size has been set by the ini file content") .isEqualTo(20000000); } } } ================================================ FILE: modules/mariadb/src/test/java/org/testcontainers/mariadb/MariaDBR2DBCDatabaseContainerTest.java ================================================ package org.testcontainers.mariadb; import io.r2dbc.spi.ConnectionFactoryOptions; import org.testcontainers.r2dbc.AbstractR2DBCDatabaseContainerTest; import org.testcontainers.utility.DockerImageName; public class MariaDBR2DBCDatabaseContainerTest extends AbstractR2DBCDatabaseContainerTest { @Override protected ConnectionFactoryOptions getOptions(MariaDBContainer container) { return MariaDBR2DBCDatabaseContainer.getOptions(container); } @Override protected String createR2DBCUrl() { return "r2dbc:tc:mariadb:///db?TC_IMAGE_TAG=10.3.39"; } @Override protected MariaDBContainer createContainer() { return new MariaDBContainer(DockerImageName.parse("mariadb:10.3.39")); } } ================================================ FILE: modules/mariadb/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/mariadb/src/test/resources/somepath/init_mariadb.sql ================================================ CREATE TABLE bar ( foo VARCHAR(255) ); INSERT INTO bar (foo) VALUES ('hello world'); ================================================ FILE: modules/mariadb/src/test/resources/somepath/init_unicode_mariadb.sql ================================================ CREATE TABLE bar ( foo varchar(255) character set utf8 ); INSERT INTO bar (foo) VALUES ('привет мир'); ================================================ FILE: modules/mariadb/src/test/resources/somepath/mariadb_conf_override/my.cnf ================================================ [mysqld] port = 3306 #socket = /tmp/mysql.sock skip-external-locking key_buffer_size = 16K max_allowed_packet = 1M table_open_cache = 4 sort_buffer_size = 64K read_buffer_size = 256K read_rnd_buffer_size = 256K net_buffer_length = 2K thread_stack = 512K skip-host-cache skip-name-resolve # This configuration is custom to test whether config override works innodb_max_undo_log_size = 20000000 # Don't listen on a TCP/IP port at all. This can be a security enhancement, # if all processes that need to connect to mysqld run on the same host. # All interaction with mysqld must be made via Unix sockets or named pipes. # Note that using this option without enabling named pipes on Windows # (using the "enable-named-pipe" option) will render mysqld useless! # #skip-networking #server-id = 1 # Uncomment the following if you want to log updates #log-bin=mysql-bin # binary logging format - mixed recommended #binlog_format=mixed # Causes updates to non-transactional engines using statement format to be # written directly to binary log. Before using this option make sure that # there are no dependencies between transactional and non-transactional # tables such as in the statement INSERT INTO t_myisam SELECT * FROM # t_innodb; otherwise, slaves may diverge from the master. #binlog_direct_non_transactional_updates=TRUE # Uncomment the following if you are using InnoDB tables innodb_data_file_path = ibdata1:10M:autoextend # You can set .._buffer_pool_size up to 50 - 80 % # of RAM but beware of setting memory usage too high innodb_buffer_pool_size = 16M #innodb_additional_mem_pool_size = 2M # Set .._log_file_size to 25 % of buffer pool size innodb_log_file_size = 5M innodb_log_buffer_size = 8M innodb_flush_log_at_trx_commit = 1 innodb_lock_wait_timeout = 50 ================================================ FILE: modules/milvus/build.gradle ================================================ description = "Testcontainers :: Milvus" dependencies { api project(':testcontainers') testImplementation 'io.milvus:milvus-sdk-java:2.6.10' } ================================================ FILE: modules/milvus/src/main/java/org/testcontainers/milvus/MilvusContainer.java ================================================ package org.testcontainers.milvus; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; /** * Testcontainers implementation for Milvus. *

* Supported image: {@code milvusdb/milvus} *

* Exposed ports: *

    *
  • Management port: 9091
  • *
  • HTTP: 19530
  • *
*/ public class MilvusContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("milvusdb/milvus"); private String etcdEndpoint; public MilvusContainer(String image) { this(DockerImageName.parse(image)); } public MilvusContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); withExposedPorts(9091, 19530); waitingFor(Wait.forHttp("/healthz").forPort(9091)); withCommand("milvus", "run", "standalone"); withCopyFileToContainer( MountableFile.forClasspathResource("testcontainers/embedEtcd.yaml"), "/milvus/configs/embedEtcd.yaml" ); withEnv("COMMON_STORAGETYPE", "local"); } @Override protected void configure() { if (this.etcdEndpoint == null) { withEnv("ETCD_USE_EMBED", "true"); withEnv("ETCD_DATA_DIR", "/var/lib/milvus/etcd"); withEnv("ETCD_CONFIG_PATH", "/milvus/configs/embedEtcd.yaml"); } else { withEnv("ETCD_ENDPOINTS", this.etcdEndpoint); } } public MilvusContainer withEtcdEndpoint(String etcdEndpoint) { this.etcdEndpoint = etcdEndpoint; return this; } public String getEndpoint() { return "http://" + getHost() + ":" + getMappedPort(19530); } } ================================================ FILE: modules/milvus/src/main/resources/testcontainers/embedEtcd.yaml ================================================ listen-client-urls: http://0.0.0.0:2379 advertise-client-urls: http://0.0.0.0:2379 ================================================ FILE: modules/milvus/src/test/java/org/testcontainers/milvus/MilvusContainerTest.java ================================================ package org.testcontainers.milvus; import io.milvus.client.MilvusServiceClient; import io.milvus.param.ConnectParam; import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.wait.strategy.Wait; import static org.assertj.core.api.Assertions.assertThat; class MilvusContainerTest { @Test void withDefaultConfig() { try ( // milvusContainer { MilvusContainer milvus = new MilvusContainer("milvusdb/milvus:v2.3.9") // } ) { milvus.start(); assertThat(milvus.getEnvMap()).doesNotContainKey("ETCD_ENDPOINTS"); assertMilvusVersion(milvus); } } @Test void withExternalEtcd() { try ( // externalEtcd { Network network = Network.newNetwork(); GenericContainer etcd = new GenericContainer<>("quay.io/coreos/etcd:v3.5.5") .withNetwork(network) .withNetworkAliases("etcd") .withCommand( "etcd", "-advertise-client-urls=http://127.0.0.1:2379", "-listen-client-urls=http://0.0.0.0:2379", "--data-dir=/etcd" ) .withEnv("ETCD_AUTO_COMPACTION_MODE", "revision") .withEnv("ETCD_AUTO_COMPACTION_RETENTION", "1000") .withEnv("ETCD_QUOTA_BACKEND_BYTES", "4294967296") .withEnv("ETCD_SNAPSHOT_COUNT", "50000") .waitingFor(Wait.forLogMessage(".*ready to serve client requests.*", 1)); MilvusContainer milvus = new MilvusContainer("milvusdb/milvus:v2.3.9") .withNetwork(network) .withEtcdEndpoint("etcd:2379") .dependsOn(etcd) // } ) { milvus.start(); assertThat(milvus.getEnvMap()).doesNotContainKey("ETCD_USE_EMBED"); assertMilvusVersion(milvus); } } private static void assertMilvusVersion(MilvusContainer milvus) { MilvusServiceClient milvusClient = new MilvusServiceClient( ConnectParam.newBuilder().withUri(milvus.getEndpoint()).build() ); assertThat(milvusClient.getVersion().getData().getVersion()).isEqualTo("v2.3.9"); } } ================================================ FILE: modules/milvus/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/minio/build.gradle ================================================ description = "Testcontainers :: MinIO" dependencies { api project(':testcontainers') testImplementation("io.minio:minio:8.6.0") } ================================================ FILE: modules/minio/src/main/java/org/testcontainers/containers/MinIOContainer.java ================================================ package org.testcontainers.containers; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import java.time.Duration; import java.time.temporal.ChronoUnit; /** * Testcontainers implementation for MinIO. *

* Supported image: {@code minio/minio} *

* Exposed ports: *

    *
  • S3: 9000
  • *
  • Console: 9001
  • *
*/ public class MinIOContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("minio/minio"); private static final int MINIO_S3_PORT = 9000; private static final int MINIO_UI_PORT = 9001; private static final String DEFAULT_USER = "minioadmin"; private static final String DEFAULT_PASSWORD = "minioadmin"; private String userName; private String password; /** * Constructs a MinIO container from the dockerImageName * @param dockerImageName the full image name to use */ public MinIOContainer(final String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } /** * Constructs a MinIO container from the dockerImageName * @param dockerImageName the full image name to use */ public MinIOContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); withExposedPorts(MINIO_S3_PORT, MINIO_UI_PORT); withCommand("server", "--console-address", ":" + MINIO_UI_PORT, "/data"); waitingFor( Wait .forHttp("/minio/health/live") .forPort(MINIO_S3_PORT) .withStartupTimeout(Duration.of(60, ChronoUnit.SECONDS)) ); } /** * Overrides the DEFAULT_USER * @param userName the Root user to override * @return this */ public MinIOContainer withUserName(String userName) { this.userName = userName; return this; } /** * Overrides the DEFAULT_PASSWORD * @param password the Root user's password to override * @return this */ public MinIOContainer withPassword(String password) { this.password = password; return this; } /** * Configures the MinIO container */ @Override public void configure() { if (this.userName != null) { addEnv("MINIO_ROOT_USER", this.userName); } else { this.userName = DEFAULT_USER; } if (this.password != null) { addEnv("MINIO_ROOT_PASSWORD", this.password); } else { this.password = DEFAULT_PASSWORD; } } /** * @return the URL to upload/download objects from */ public String getS3URL() { return String.format("http://%s:%s", this.getHost(), getMappedPort(MINIO_S3_PORT)); } /** * @return the Username for the Root user */ public String getUserName() { return this.userName; } /** * @return the password for the Root user */ public String getPassword() { return this.password; } } ================================================ FILE: modules/minio/src/test/java/org/testcontainers/containers/MinIOContainerTest.java ================================================ package org.testcontainers.containers; import io.minio.BucketExistsArgs; import io.minio.MakeBucketArgs; import io.minio.MinioClient; import io.minio.StatObjectArgs; import io.minio.StatObjectResponse; import io.minio.UploadObjectArgs; import org.junit.jupiter.api.Test; import java.net.URL; import static org.assertj.core.api.Assertions.assertThat; class MinIOContainerTest { @Test void testBasicUsage() throws Exception { try ( // minioContainer { MinIOContainer container = new MinIOContainer("minio/minio:RELEASE.2023-09-04T19-57-37Z"); // } ) { container.start(); // configuringClient { MinioClient minioClient = MinioClient .builder() .endpoint(container.getS3URL()) .credentials(container.getUserName(), container.getPassword()) .build(); // } minioClient.makeBucket(MakeBucketArgs.builder().bucket("test-bucket").region("us-west-2").build()); BucketExistsArgs existsArgs = BucketExistsArgs.builder().bucket("test-bucket").build(); assertThat(minioClient.bucketExists(existsArgs)).isTrue(); URL file = this.getClass().getResource("/object_to_upload.txt"); assertThat(file).isNotNull(); minioClient.uploadObject( UploadObjectArgs .builder() .bucket("test-bucket") .object("my-objectname") .filename(file.getPath()) .build() ); StatObjectResponse objectStat = minioClient.statObject( StatObjectArgs.builder().bucket("test-bucket").object("my-objectname").build() ); assertThat(objectStat.object()).isEqualTo("my-objectname"); } } @Test void testDefaultUserPassword() { try (MinIOContainer container = new MinIOContainer("minio/minio:RELEASE.2023-09-04T19-57-37Z")) { container.start(); assertThat(container.getUserName()).isEqualTo("minioadmin"); assertThat(container.getPassword()).isEqualTo("minioadmin"); } } @Test void testOverwriteUserPassword() { try ( // minioOverrides { MinIOContainer container = new MinIOContainer("minio/minio:RELEASE.2023-09-04T19-57-37Z") .withUserName("testuser") .withPassword("testpassword"); // } ) { container.start(); assertThat(container.getUserName()).isEqualTo("testuser"); assertThat(container.getPassword()).isEqualTo("testpassword"); } } } ================================================ FILE: modules/minio/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/minio/src/test/resources/object_to_upload.txt ================================================ This is a file ================================================ FILE: modules/mockserver/build.gradle ================================================ description = "Testcontainers :: MockServer" dependencies { api project(':testcontainers') testImplementation 'org.mock-server:mockserver-client-java:5.15.0' testImplementation 'io.rest-assured:rest-assured:5.5.6' } ================================================ FILE: modules/mockserver/src/main/java/org/testcontainers/containers/MockServerContainer.java ================================================ package org.testcontainers.containers; import lombok.extern.slf4j.Slf4j; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; /** * @deprecated use {@link org.testcontainers.mockserver.MockServerContainer} instead. */ @Slf4j @Deprecated public class MockServerContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("jamesdbloom/mockserver"); private static final String DEFAULT_TAG = "mockserver-5.5.4"; @Deprecated public static final String VERSION = DEFAULT_TAG; public static final int PORT = 1080; /** * @deprecated use {@link #MockServerContainer(DockerImageName)} instead */ @Deprecated public MockServerContainer() { this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } /** * @deprecated use {@link #MockServerContainer(DockerImageName)} instead */ @Deprecated public MockServerContainer(String version) { this(DEFAULT_IMAGE_NAME.withTag("mockserver-" + version)); } public MockServerContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, DockerImageName.parse("mockserver/mockserver")); waitingFor(Wait.forLogMessage(".*started on port: " + PORT + ".*", 1)); withCommand("-serverPort " + PORT); addExposedPorts(PORT); } public String getEndpoint() { return String.format("http://%s:%d", getHost(), getMappedPort(PORT)); } public String getSecureEndpoint() { return String.format("https://%s:%d", getHost(), getMappedPort(PORT)); } public Integer getServerPort() { return getMappedPort(PORT); } } ================================================ FILE: modules/mockserver/src/main/java/org/testcontainers/mockserver/MockServerContainer.java ================================================ package org.testcontainers.mockserver; import lombok.extern.slf4j.Slf4j; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; @Slf4j public class MockServerContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("jamesdbloom/mockserver"); private static final String DEFAULT_TAG = "mockserver-5.5.4"; @Deprecated public static final String VERSION = DEFAULT_TAG; public static final int PORT = 1080; /** * @deprecated use {@link #MockServerContainer(DockerImageName)} instead */ @Deprecated public MockServerContainer(String version) { this(DEFAULT_IMAGE_NAME.withTag("mockserver-" + version)); } public MockServerContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, DockerImageName.parse("mockserver/mockserver")); waitingFor(Wait.forLogMessage(".*started on port: " + PORT + ".*", 1)); withCommand("-serverPort " + PORT); addExposedPorts(PORT); } public String getEndpoint() { return String.format("http://%s:%d", getHost(), getMappedPort(PORT)); } public String getSecureEndpoint() { return String.format("https://%s:%d", getHost(), getMappedPort(PORT)); } public Integer getServerPort() { return getMappedPort(PORT); } } ================================================ FILE: modules/mockserver/src/test/java/org/testcontainers/mockserver/MockServerContainerTest.java ================================================ package org.testcontainers.mockserver; import io.restassured.config.RestAssuredConfig; import io.restassured.config.SSLConfig; import org.apache.http.conn.ssl.SSLSocketFactory; import org.junit.jupiter.api.Test; import org.mockserver.client.MockServerClient; import org.mockserver.configuration.Configuration; import org.mockserver.logging.MockServerLogger; import org.mockserver.socket.tls.KeyStoreFactory; import org.testcontainers.utility.DockerImageName; import static io.restassured.RestAssured.given; import static org.assertj.core.api.Assertions.assertThat; import static org.mockserver.model.HttpRequest.request; import static org.mockserver.model.HttpResponse.response; class MockServerContainerTest { public static final DockerImageName MOCKSERVER_IMAGE = DockerImageName .parse("mockserver/mockserver") .withTag("mockserver-" + MockServerClient.class.getPackage().getImplementationVersion()); @Test void shouldCallActualMockserverVersion() { try ( // creatingProxy { MockServerContainer mockServer = new MockServerContainer(MOCKSERVER_IMAGE) // } ) { mockServer.start(); String expectedBody = "Hello World!"; try (MockServerClient client = new MockServerClient(mockServer.getHost(), mockServer.getServerPort())) { assertThat(client.hasStarted()).as("Mockserver running").isTrue(); client.when(request().withPath("/hello")).respond(response().withBody(expectedBody)); assertThat(given().when().get(mockServer.getEndpoint() + "/hello").then().extract().body().asString()) .as("MockServer returns correct result") .isEqualTo(expectedBody); } } } @Test void shouldCallMockserverUsingTlsProtocol() { try (MockServerContainer mockServer = new MockServerContainer(MOCKSERVER_IMAGE)) { mockServer.start(); String expectedBody = "Hello World!"; try ( MockServerClient client = new MockServerClient(mockServer.getHost(), mockServer.getServerPort()) .withSecure(true) ) { assertThat(client.hasStarted()).as("Mockserver running").isTrue(); client.when(request().withPath("/hello")).respond(response().withBody(expectedBody)); assertThat(secureResponseFromMockserver(mockServer)) .as("MockServer returns correct result") .isEqualTo(expectedBody); } } } @Test void shouldCallMockserverUsingMutualTlsProtocol() { try ( MockServerContainer mockServer = new MockServerContainer(MOCKSERVER_IMAGE) .withEnv("MOCKSERVER_TLS_MUTUAL_AUTHENTICATION_REQUIRED", "true") ) { mockServer.start(); String expectedBody = "Hello World!"; try ( MockServerClient client = new MockServerClient(mockServer.getHost(), mockServer.getServerPort()) .withSecure(true) ) { assertThat(client.hasStarted()).as("Mockserver running").isTrue(); client.when(request().withPath("/hello")).respond(response().withBody(expectedBody)); assertThat(secureResponseFromMockserver(mockServer)) .as("MockServer returns correct result") .isEqualTo(expectedBody); } } } @Test void newVersionStartsWithDefaultWaitStrategy() { try (MockServerContainer mockServer = new MockServerContainer(MOCKSERVER_IMAGE)) { mockServer.start(); } } private static String secureResponseFromMockserver(MockServerContainer mockServer) { return given() .config( RestAssuredConfig .config() .sslConfig( SSLConfig .sslConfig() .sslSocketFactory( new SSLSocketFactory( new KeyStoreFactory(Configuration.configuration(), new MockServerLogger()) .sslContext() ) ) ) ) .baseUri(mockServer.getSecureEndpoint()) .get("/hello") .body() .asString(); } } ================================================ FILE: modules/mockserver/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/mongodb/build.gradle ================================================ description = "Testcontainers :: MongoDB" dependencies { api project(':testcontainers') testImplementation("org.mongodb:mongodb-driver-sync:5.1.4") } ================================================ FILE: modules/mongodb/src/main/java/org/testcontainers/containers/MongoDBContainer.java ================================================ package org.testcontainers.containers; import com.github.dockerjava.api.command.InspectContainerResponse; import lombok.NonNull; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.io.IOException; /** * Testcontainers implementation for MongoDB. *

* Supported images: {@code mongo}, {@code mongodb/mongodb-community-server}, {@code mongodb/mongodb-enterprise-server} *

* Exposed ports: 27017 * * @deprecated use {@link org.testcontainers.mongodb.MongoDBContainer} instead. */ @Slf4j @Deprecated public class MongoDBContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("mongo"); private static final DockerImageName COMMUNITY_SERVER_IMAGE = DockerImageName.parse( "mongodb/mongodb-community-server" ); private static final DockerImageName ENTERPRISE_SERVER_IMAGE = DockerImageName.parse( "mongodb/mongodb-enterprise-server" ); private static final String DEFAULT_TAG = "4.0.10"; private static final int CONTAINER_EXIT_CODE_OK = 0; private static final int AWAIT_INIT_REPLICA_SET_ATTEMPTS = 60; private static final String MONGODB_DATABASE_NAME_DEFAULT = "test"; private static final String STARTER_SCRIPT = "/testcontainers_start.sh"; private boolean shardingEnabled; /** * @deprecated use {@link #MongoDBContainer(DockerImageName)} instead */ @Deprecated public MongoDBContainer() { this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } public MongoDBContainer(@NonNull final String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public MongoDBContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, COMMUNITY_SERVER_IMAGE, ENTERPRISE_SERVER_IMAGE); } @Override MongoDBContainerDef createContainerDef() { return new MongoDBContainerDef(); } @Override MongoDBContainerDef getContainerDef() { return (MongoDBContainerDef) super.getContainerDef(); } @Override protected void containerIsStarting(InspectContainerResponse containerInfo) { if (this.shardingEnabled) { copyFileToContainer(MountableFile.forClasspathResource("/sharding.sh", 0777), STARTER_SCRIPT); } } /** * Enables sharding on the cluster * * @return this */ public MongoDBContainer withSharding() { this.shardingEnabled = true; getContainerDef().withSharding(); return this; } @Override protected void containerIsStarted(InspectContainerResponse containerInfo, boolean reused) { if (!this.shardingEnabled) { initReplicaSet(reused); } } /** * Gets a connection string url, unlike {@link #getReplicaSetUrl} this does not point to a * database * @return a connection url pointing to a mongodb instance */ public String getConnectionString() { return String.format("mongodb://%s:%d", getHost(), getMappedPort(MongoDBContainerDef.MONGODB_INTERNAL_PORT)); } /** * Gets a replica set url for the default {@value #MONGODB_DATABASE_NAME_DEFAULT} database. * * @return a replica set url. */ public String getReplicaSetUrl() { return getReplicaSetUrl(MONGODB_DATABASE_NAME_DEFAULT); } /** * Gets a replica set url for a provided databaseName. * * @param databaseName a database name. * @return a replica set url. */ public String getReplicaSetUrl(final String databaseName) { if (!isRunning()) { throw new IllegalStateException("MongoDBContainer should be started first"); } return getConnectionString() + "/" + databaseName; } private String[] buildMongoEvalCommand(final String command) { return new String[] { "sh", "-c", "mongosh mongo --eval \"" + command + "\" || mongo --eval \"" + command + "\"", }; } private void checkMongoNodeExitCode(final Container.ExecResult execResult) { if (execResult.getExitCode() != CONTAINER_EXIT_CODE_OK) { final String errorMessage = String.format("An error occurred: %s", execResult.getStdout()); log.error(errorMessage); throw new ReplicaSetInitializationException(errorMessage); } } private String buildMongoWaitCommand() { return String.format( "var attempt = 0; " + "while" + "(%s) " + "{ " + "if (attempt > %d) {quit(1);} " + "print('%s ' + attempt); sleep(100); attempt++; " + " }", "db.runCommand( { isMaster: 1 } ).ismaster==false", AWAIT_INIT_REPLICA_SET_ATTEMPTS, "An attempt to await for a single node replica set initialization:" ); } private void checkMongoNodeExitCodeAfterWaiting(final Container.ExecResult execResultWaitForMaster) { if (execResultWaitForMaster.getExitCode() != CONTAINER_EXIT_CODE_OK) { final String errorMessage = String.format( "A single node replica set was not initialized in a set timeout: %d attempts", AWAIT_INIT_REPLICA_SET_ATTEMPTS ); log.error(errorMessage); throw new ReplicaSetInitializationException(errorMessage); } } @SneakyThrows(value = { IOException.class, InterruptedException.class }) private void initReplicaSet(boolean reused) { if (reused && isReplicationSetAlreadyInitialized()) { log.debug("Replica set already initialized."); } else { log.debug("Initializing a single node node replica set..."); final ExecResult execResultInitRs = execInContainer(buildMongoEvalCommand("rs.initiate();")); log.debug(execResultInitRs.getStdout()); checkMongoNodeExitCode(execResultInitRs); log.debug( "Awaiting for a single node replica set initialization up to {} attempts", AWAIT_INIT_REPLICA_SET_ATTEMPTS ); final ExecResult execResultWaitForMaster = execInContainer(buildMongoEvalCommand(buildMongoWaitCommand())); log.debug(execResultWaitForMaster.getStdout()); checkMongoNodeExitCodeAfterWaiting(execResultWaitForMaster); } } public static class ReplicaSetInitializationException extends RuntimeException { ReplicaSetInitializationException(final String errorMessage) { super(errorMessage); } } @SneakyThrows private boolean isReplicationSetAlreadyInitialized() { // since we are creating a replica set with one node, this node must be primary (state = 1) final ExecResult execCheckRsInit = execInContainer( buildMongoEvalCommand("if(db.adminCommand({replSetGetStatus: 1})['myState'] != 1) quit(900)") ); return execCheckRsInit.getExitCode() == CONTAINER_EXIT_CODE_OK; } private static class MongoDBContainerDef extends ContainerDef { private static final int MONGODB_INTERNAL_PORT = 27017; MongoDBContainerDef() { addExposedTcpPort(MONGODB_INTERNAL_PORT); setCommand("--replSet", "docker-rs"); setWaitStrategy(Wait.forLogMessage("(?i).*waiting for connections.*", 1)); } void withSharding() { setCommand("-c", "while [ ! -f " + STARTER_SCRIPT + " ]; do sleep 0.1; done; " + STARTER_SCRIPT); setWaitStrategy(Wait.forLogMessage("(?i).*mongos ready.*", 1)); setEntrypoint("sh"); } } } ================================================ FILE: modules/mongodb/src/main/java/org/testcontainers/mongodb/MongoDBAtlasLocalContainer.java ================================================ package org.testcontainers.mongodb; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; /** * Testcontainers implementation for MongoDB Atlas. *

* Supported images: {@code mongodb/mongodb-atlas-local} *

* Exposed ports: 27017 */ public class MongoDBAtlasLocalContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("mongodb/mongodb-atlas-local"); private static final int MONGODB_INTERNAL_PORT = 27017; private static final String MONGODB_DATABASE_NAME_DEFAULT = "test"; private static final String DIRECT_CONNECTION = "directConnection=true"; public MongoDBAtlasLocalContainer(final String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public MongoDBAtlasLocalContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); withExposedPorts(MONGODB_INTERNAL_PORT); waitingFor(Wait.forSuccessfulCommand("runner healthcheck")); } /** * Get the connection string to MongoDB. */ public String getConnectionString() { return baseConnectionString() + "/?" + DIRECT_CONNECTION; } private String baseConnectionString() { return String.format("mongodb://%s:%d", getHost(), getMappedPort(MONGODB_INTERNAL_PORT)); } /** * Gets a database specific connection string for the default {@value #MONGODB_DATABASE_NAME_DEFAULT} database. * * @return a database specific connection string. */ public String getDatabaseConnectionString() { return getDatabaseConnectionString(MONGODB_DATABASE_NAME_DEFAULT); } /** * Gets a database specific connection string for a provided databaseName. * * @param databaseName a database name. * @return a database specific connection string. */ public String getDatabaseConnectionString(final String databaseName) { if (!isRunning()) { throw new IllegalStateException("MongoDBContainer should be started first"); } return baseConnectionString() + "/" + databaseName + "?" + DIRECT_CONNECTION; } } ================================================ FILE: modules/mongodb/src/main/java/org/testcontainers/mongodb/MongoDBContainer.java ================================================ package org.testcontainers.mongodb; import com.github.dockerjava.api.command.InspectContainerResponse; import lombok.NonNull; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.io.IOException; /** * Testcontainers implementation for MongoDB. *

* Supported images: {@code mongo}, {@code mongodb/mongodb-community-server}, {@code mongodb/mongodb-enterprise-server} *

* Exposed ports: 27017 */ @Slf4j public class MongoDBContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("mongo"); private static final DockerImageName COMMUNITY_SERVER_IMAGE = DockerImageName.parse( "mongodb/mongodb-community-server" ); private static final DockerImageName ENTERPRISE_SERVER_IMAGE = DockerImageName.parse( "mongodb/mongodb-enterprise-server" ); private static final int MONGODB_INTERNAL_PORT = 27017; private static final int CONTAINER_EXIT_CODE_OK = 0; private static final int AWAIT_INIT_REPLICA_SET_ATTEMPTS = 60; private static final String MONGODB_DATABASE_NAME_DEFAULT = "test"; private static final String STARTER_SCRIPT = "/testcontainers_start.sh"; private boolean shardingEnabled; private boolean rsEnabled; public MongoDBContainer(@NonNull String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public MongoDBContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, COMMUNITY_SERVER_IMAGE, ENTERPRISE_SERVER_IMAGE); withExposedPorts(MONGODB_INTERNAL_PORT); } @Override protected void containerIsStarting(InspectContainerResponse containerInfo) { if (this.shardingEnabled) { copyFileToContainer(MountableFile.forClasspathResource("/sharding.sh", 0777), STARTER_SCRIPT); } } @Override protected void containerIsStarted(InspectContainerResponse containerInfo, boolean reused) { if (this.rsEnabled) { initReplicaSet(reused); } } private String[] buildMongoEvalCommand(String command) { return new String[] { "sh", "-c", "mongosh mongo --eval \"" + command + "\" || mongo --eval \"" + command + "\"", }; } private void checkMongoNodeExitCode(ExecResult execResult) { if (execResult.getExitCode() != CONTAINER_EXIT_CODE_OK) { String errorMessage = String.format("An error occurred: %s", execResult.getStdout()); log.error(errorMessage); throw new ReplicaSetInitializationException(errorMessage); } } private String buildMongoWaitCommand() { return String.format( "var attempt = 0; " + "while" + "(%s) " + "{ " + "if (attempt > %d) {quit(1);} " + "print('%s ' + attempt); sleep(100); attempt++; " + " }", "db.runCommand( { isMaster: 1 } ).ismaster==false", AWAIT_INIT_REPLICA_SET_ATTEMPTS, "An attempt to await for a single node replica set initialization:" ); } private void checkMongoNodeExitCodeAfterWaiting(ExecResult execResultWaitForMaster) { if (execResultWaitForMaster.getExitCode() != CONTAINER_EXIT_CODE_OK) { String errorMessage = String.format( "A single node replica set was not initialized in a set timeout: %d attempts", AWAIT_INIT_REPLICA_SET_ATTEMPTS ); log.error(errorMessage); throw new ReplicaSetInitializationException(errorMessage); } } @SneakyThrows(value = { IOException.class, InterruptedException.class }) private void initReplicaSet(boolean reused) { if (reused && isReplicationSetAlreadyInitialized()) { log.debug("Replica set already initialized."); } else { log.debug("Initializing a single node node replica set..."); ExecResult execResultInitRs = execInContainer(buildMongoEvalCommand("rs.initiate();")); log.debug(execResultInitRs.getStdout()); checkMongoNodeExitCode(execResultInitRs); log.debug( "Awaiting for a single node replica set initialization up to {} attempts", AWAIT_INIT_REPLICA_SET_ATTEMPTS ); ExecResult execResultWaitForMaster = execInContainer(buildMongoEvalCommand(buildMongoWaitCommand())); log.debug(execResultWaitForMaster.getStdout()); checkMongoNodeExitCodeAfterWaiting(execResultWaitForMaster); } } public static class ReplicaSetInitializationException extends RuntimeException { ReplicaSetInitializationException(String errorMessage) { super(errorMessage); } } @SneakyThrows private boolean isReplicationSetAlreadyInitialized() { // since we are creating a replica set with one node, this node must be primary (state = 1) ExecResult execCheckRsInit = execInContainer( buildMongoEvalCommand("if(db.adminCommand({replSetGetStatus: 1})['myState'] != 1) quit(900)") ); return execCheckRsInit.getExitCode() == CONTAINER_EXIT_CODE_OK; } /** * Enables replica set on the cluster * * @return this */ public MongoDBContainer withReplicaSet() { this.rsEnabled = true; withCommand("--replSet", "docker-rs"); waitingFor(Wait.forLogMessage("(?i).*waiting for connections.*", 1)); return this; } /** * Enables sharding on the cluster * * @return this */ public MongoDBContainer withSharding() { this.shardingEnabled = true; withCommand("-c", "while [ ! -f " + STARTER_SCRIPT + " ]; do sleep 0.1; done; " + STARTER_SCRIPT); waitingFor(Wait.forLogMessage("(?i).*mongos ready.*", 1)); withCreateContainerCmdModifier(cmd -> cmd.withEntrypoint("sh")); return this; } /** * Gets a connection string url, unlike {@link #getReplicaSetUrl} this does not point to a * database * @return a connection url pointing to a mongodb instance */ public String getConnectionString() { return String.format("mongodb://%s:%d", getHost(), getMappedPort(MONGODB_INTERNAL_PORT)); } /** * Gets a replica set url for the default {@value #MONGODB_DATABASE_NAME_DEFAULT} database. * * @return a replica set url. */ public String getReplicaSetUrl() { return getReplicaSetUrl(MONGODB_DATABASE_NAME_DEFAULT); } /** * Gets a replica set url for a provided databaseName. * * @param databaseName a database name. * @return a replica set url. */ public String getReplicaSetUrl(String databaseName) { if (!isRunning()) { throw new IllegalStateException("MongoDBContainer should be started first"); } return getConnectionString() + "/" + databaseName; } } ================================================ FILE: modules/mongodb/src/main/resources/sharding.sh ================================================ #!/bin/bash CONFIGSVR=/tmp/mongod/configsvr SHARDSVR=/tmp/mongod/shardsvr function retry() { COUNT=${COUNT:-0} if [ $COUNT == 5 ] then echo Failed $COUNT attempts exit 1 fi sleep $COUNT echo "Attempt #$[ $COUNT + 1 ] '$*' " eval $* if [ $? -ne 0 ] then COUNT=$[ $COUNT + 1 ] retry $* fi unset COUNT } function initReplSet() { PORT=$1 COUNT=${2:-1} CMD="mongosh --quiet --port $PORT --eval \"if(db.adminCommand({replSetGetStatus: 1})['myState'] != 1) quit(900)\"" eval $CMD retVal=$? if [ $retVal -ne 0 -a $COUNT -ne 5 ] then echo "Initiating replSet (attempt $COUNT)" mongosh --quiet --port $PORT --eval 'rs.initiate();' if [ $? -ne 0 ] then sleep $COUNT initReplSet $PORT $[ $COUNT + 1 ] fi fi unset COUNT } rm -rf $CONFIGSVR $SHARDSVR mkdir -p $CONFIGSVR mkdir -p $SHARDSVR echo "Starting configsvr" mongod --bind_ip_all --configsvr --port 27019 --replSet configsvr-rs --dbpath $CONFIGSVR --logpath /tmp/configsvr.log & echo "Initiating configsvr replSet" initReplSet 27019 echo "Starting shardsvr" mongod --bind_ip_all --shardsvr --port 27018 --replSet shardsvr-rs --dbpath $SHARDSVR --logpath /tmp/shardsvr.log & echo "Initiating shardsvr replSet" initReplSet 27018 echo "Starting mongos" mongos --bind_ip_all --configdb configsvr-rs/localhost:27019 --logpath /tmp/mongos.log & echo "Adding a shard" retry "mongosh --eval 'sh.addShard(\"shardsvr-rs/`hostname`:27018\");'" echo "mongos ready" mongosh --quiet tctest --eval "db.testcollection.insertOne({});" sleep 36000 ================================================ FILE: modules/mongodb/src/test/java/org/testcontainers/mongodb/AbstractMongo.java ================================================ package org.testcontainers.mongodb; import com.mongodb.ReadConcern; import com.mongodb.ReadPreference; import com.mongodb.TransactionOptions; import com.mongodb.WriteConcern; import com.mongodb.client.ClientSession; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; import com.mongodb.client.MongoCollection; import com.mongodb.client.TransactionBody; import org.bson.Document; import static org.assertj.core.api.Assertions.assertThat; public class AbstractMongo { protected void executeTx(MongoDBContainer mongoDBContainer) { final MongoClient mongoSyncClientBase = MongoClients.create(mongoDBContainer.getConnectionString()); final MongoClient mongoSyncClient = MongoClients.create(mongoDBContainer.getReplicaSetUrl()); mongoSyncClient .getDatabase("mydb1") .getCollection("foo") .withWriteConcern(WriteConcern.MAJORITY) .insertOne(new Document("abc", 0)); mongoSyncClient .getDatabase("mydb2") .getCollection("bar") .withWriteConcern(WriteConcern.MAJORITY) .insertOne(new Document("xyz", 0)); mongoSyncClientBase .getDatabase("mydb3") .getCollection("baz") .withWriteConcern(WriteConcern.MAJORITY) .insertOne(new Document("def", 0)); final ClientSession clientSession = mongoSyncClient.startSession(); final TransactionOptions txnOptions = TransactionOptions .builder() .readPreference(ReadPreference.primary()) .readConcern(ReadConcern.LOCAL) .writeConcern(WriteConcern.MAJORITY) .build(); final String trxResult = "Inserted into collections in different databases"; TransactionBody txnBody = () -> { final MongoCollection coll1 = mongoSyncClient.getDatabase("mydb1").getCollection("foo"); final MongoCollection coll2 = mongoSyncClient.getDatabase("mydb2").getCollection("bar"); coll1.insertOne(clientSession, new Document("abc", 1)); coll2.insertOne(clientSession, new Document("xyz", 999)); return trxResult; }; try { final String trxResultActual = clientSession.withTransaction(txnBody, txnOptions); assertThat(trxResultActual).isEqualTo(trxResult); } catch (RuntimeException re) { throw new IllegalStateException(re.getMessage(), re); } finally { clientSession.close(); mongoSyncClient.close(); } } } ================================================ FILE: modules/mongodb/src/test/java/org/testcontainers/mongodb/AtlasLocalDataAccess.java ================================================ package org.testcontainers.mongodb; import com.mongodb.ConnectionString; import com.mongodb.MongoClientSettings; import com.mongodb.client.ListSearchIndexesIterable; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; import com.mongodb.client.model.Aggregates; import com.mongodb.client.model.search.SearchOperator; import com.mongodb.client.model.search.SearchOptions; import com.mongodb.client.model.search.SearchPath; import org.bson.BsonDocument; import org.bson.Document; import org.bson.codecs.configuration.CodecRegistries; import org.bson.codecs.configuration.CodecRegistry; import org.bson.codecs.pojo.PojoCodecProvider; import org.bson.conversions.Bson; import org.bson.json.JsonWriterSettings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.concurrent.TimeUnit; import static org.awaitility.Awaitility.await; public class AtlasLocalDataAccess implements AutoCloseable { private static final Logger log = LoggerFactory.getLogger(AtlasLocalDataAccess.class); private final MongoClient mongoClient; private final MongoDatabase testDB; private final MongoCollection testCollection; private final String collectionName; public AtlasLocalDataAccess(String connectionString, String databaseName, String collectionName) { this.collectionName = collectionName; log.info("DataAccess connecting to {}", connectionString); CodecRegistry pojoCodecRegistry = CodecRegistries.fromProviders( PojoCodecProvider.builder().automatic(true).build() ); CodecRegistry codecRegistry = CodecRegistries.fromRegistries( MongoClientSettings.getDefaultCodecRegistry(), pojoCodecRegistry ); MongoClientSettings clientSettings = MongoClientSettings .builder() .applyConnectionString(new ConnectionString(connectionString)) .codecRegistry(codecRegistry) .build(); mongoClient = MongoClients.create(clientSettings); testDB = mongoClient.getDatabase(databaseName); testCollection = testDB.getCollection(collectionName, TestData.class); } @Override public void close() { mongoClient.close(); } public void initAtlasSearchIndex() throws URISyntaxException, IOException, InterruptedException { //Create the collection (if it doesn't exist). Required because unlike other database operations, createSearchIndex will fail if the collection doesn't exist yet testDB.createCollection(collectionName); //Read the atlas search index JSON from a resource file String atlasSearchIndexJson = new String( Files.readAllBytes(Paths.get(getClass().getResource("/atlas-local-index.json").toURI())), StandardCharsets.UTF_8 ); log.info( "Creating Atlas Search index AtlasSearchIndex on collection {}:\n{}", collectionName, atlasSearchIndexJson ); testCollection.createSearchIndex("AtlasSearchIndex", BsonDocument.parse(atlasSearchIndexJson)); //wait for the atlas search index to be ready Instant start = Instant.now(); await() .atMost(5, TimeUnit.SECONDS) .pollInterval(10, TimeUnit.MILLISECONDS) .pollInSameThread() .until(this::getIndexStatus, "READY"::equalsIgnoreCase); log.info( "Atlas Search index AtlasSearchIndex on collection {} is ready (took {} milliseconds) to create.", collectionName, start.until(Instant.now(), ChronoUnit.MILLIS) ); } private String getIndexStatus() { ListSearchIndexesIterable searchIndexes = testCollection.listSearchIndexes(); for (Document searchIndex : searchIndexes) { if (searchIndex.get("name").equals("AtlasSearchIndex")) { return searchIndex.getString("status"); } } return null; } public void insertData(TestData data) { log.info("Inserting document {}", data); testCollection.insertOne(data); } public TestData findAtlasSearch(String test) { Bson searchClause = Aggregates.search( SearchOperator.of(SearchOperator.text(SearchPath.fieldPath("test"), test).fuzzy()), SearchOptions.searchOptions().index("AtlasSearchIndex") ); log.trace( "Searching for document using Atlas Search:\n{}", searchClause.toBsonDocument().toJson(JsonWriterSettings.builder().indent(true).build()) ); return testCollection.aggregate(Collections.singletonList(searchClause)).first(); } public static class TestData { String test; int test2; boolean test3; public TestData() {} public TestData(String test, int test2, boolean test3) { this.test = test; this.test2 = test2; this.test3 = test3; } public String getTest() { return test; } public void setTest(String test) { this.test = test; } public int getTest2() { return test2; } public void setTest2(int test2) { this.test2 = test2; } public boolean isTest3() { return test3; } public void setTest3(boolean test3) { this.test3 = test3; } @Override public String toString() { return "TestData{" + "test='" + test + '\'' + ", test2=" + test2 + ", test3=" + test3 + '}'; } } } ================================================ FILE: modules/mongodb/src/test/java/org/testcontainers/mongodb/CompatibleImageTest.java ================================================ package org.testcontainers.mongodb; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; import org.bson.Document; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import static org.assertj.core.api.Assertions.assertThat; class CompatibleImageTest extends AbstractMongo { static String[] image() { return new String[] { "mongo:7", "mongodb/mongodb-community-server:7.0.2-ubi8", "mongodb/mongodb-enterprise-server:7.0.0-ubi8", }; } @Test void shouldExecuteTransactions() { try ( // creatingMongoDBContainer { MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.0.10").withReplicaSet() // } ) { // startingMongoDBContainer { mongoDBContainer.start(); // } executeTx(mongoDBContainer); } } @ParameterizedTest @MethodSource("image") void shouldSupportSharding(String image) { try (MongoDBContainer mongoDBContainer = new MongoDBContainer(image).withSharding()) { mongoDBContainer.start(); final MongoClient mongoClient = MongoClients.create(mongoDBContainer.getReplicaSetUrl()); mongoClient.getDatabase("mydb1").getCollection("foo").insertOne(new Document("abc", 0)); Document shards = mongoClient.getDatabase("config").getCollection("shards").find().first(); assertThat(shards).isNotNull(); assertThat(shards).isNotEmpty(); assertThat(isReplicaSet(mongoClient)).isFalse(); } } private boolean isReplicaSet(MongoClient mongoClient) { return runIsMaster(mongoClient).get("setName") != null; } private Document runIsMaster(MongoClient mongoClient) { return mongoClient.getDatabase("admin").runCommand(new Document("ismaster", 1)); } } ================================================ FILE: modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBAtlasLocalContainerTest.java ================================================ package org.testcontainers.mongodb; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Objects; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; class MongoDBAtlasLocalContainerTest { private static final Logger log = LoggerFactory.getLogger(MongoDBAtlasLocalContainerTest.class); @Test void getConnectionString() { try ( MongoDBAtlasLocalContainer container = new MongoDBAtlasLocalContainer("mongodb/mongodb-atlas-local:7.0.9") ) { container.start(); String connectionString = container.getConnectionString(); assertThat(connectionString).isNotNull(); assertThat(connectionString).startsWith("mongodb://"); assertThat(connectionString) .isEqualTo( String.format( "mongodb://%s:%d/?directConnection=true", container.getHost(), container.getFirstMappedPort() ) ); } } @Test void getDatabaseConnectionString() { try ( MongoDBAtlasLocalContainer container = new MongoDBAtlasLocalContainer("mongodb/mongodb-atlas-local:7.0.9") ) { container.start(); String databaseConnectionString = container.getDatabaseConnectionString(); assertThat(databaseConnectionString).isNotNull(); assertThat(databaseConnectionString).startsWith("mongodb://"); assertThat(databaseConnectionString) .isEqualTo( String.format( "mongodb://%s:%d/test?directConnection=true", container.getHost(), container.getFirstMappedPort() ) ); } } @Test void createAtlasIndexAndSearchIt() throws Exception { try ( // creatingAtlasLocalContainer { MongoDBAtlasLocalContainer atlasLocalContainer = new MongoDBAtlasLocalContainer( "mongodb/mongodb-atlas-local:7.0.9" ); // } ) { // startingAtlasLocalContainer { atlasLocalContainer.start(); // } // getConnectionStringAtlasLocalContainer { String connectionString = atlasLocalContainer.getConnectionString(); // } try ( AtlasLocalDataAccess atlasLocalDataAccess = new AtlasLocalDataAccess(connectionString, "test", "test") ) { atlasLocalDataAccess.initAtlasSearchIndex(); atlasLocalDataAccess.insertData(new AtlasLocalDataAccess.TestData("tests", 123, true)); Instant start = Instant.now(); log.info( "Waiting for Atlas Search to index the data by polling atlas search query (Atlas Search is eventually consistent)" ); await() .atMost(5, TimeUnit.SECONDS) .pollInterval(10, TimeUnit.MILLISECONDS) .pollInSameThread() .until(() -> atlasLocalDataAccess.findAtlasSearch("test"), Objects::nonNull); log.info( "Atlas Search indexed the new data and was searchable after {}ms.", start.until(Instant.now(), ChronoUnit.MILLIS) ); } } } } ================================================ FILE: modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBContainerTest.java ================================================ package org.testcontainers.mongodb; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class MongoDBContainerTest extends AbstractMongo { /** * Taken from https://docs.mongodb.com */ @Test void shouldExecuteTransactions() { try ( // creatingMongoDBContainer { MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.0.10").withReplicaSet() // } ) { // startingMongoDBContainer { mongoDBContainer.start(); // } executeTx(mongoDBContainer); } } @Test void supportsMongoDB_7_0() { try (MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:7.0")) { mongoDBContainer.start(); } } @Test void shouldTestDatabaseName() { try (MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.0.10")) { mongoDBContainer.start(); final String databaseName = "my-db"; assertThat(mongoDBContainer.getReplicaSetUrl(databaseName)).endsWith(databaseName); } } } ================================================ FILE: modules/mongodb/src/test/resources/atlas-local-index.json ================================================ { "mappings": { "dynamic": false, "fields": { "test": { "type": "string" }, "test2": { "type": "number", "representation": "int64", "indexDoubles": false }, "test3": { "type": "boolean" } } } } ================================================ FILE: modules/mongodb/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/mssqlserver/AUTHORS ================================================ Stefan Hufschmidt ================================================ FILE: modules/mssqlserver/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2017 - 2019 G DATA Software AG and other authors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: modules/mssqlserver/build.gradle ================================================ description = "Testcontainers :: MS SQL Server" dependencies { api project(':testcontainers-jdbc') compileOnly project(':testcontainers-r2dbc') compileOnly 'io.r2dbc:r2dbc-mssql:1.0.3.RELEASE' testImplementation project(':testcontainers-jdbc-test') testImplementation 'com.microsoft.sqlserver:mssql-jdbc:13.3.0.jre8-preview' testImplementation project(':testcontainers-r2dbc') testRuntimeOnly 'io.r2dbc:r2dbc-mssql:1.0.3.RELEASE' // MSSQL's wait strategy requires the JDBC driver testImplementation testFixtures(project(':testcontainers-r2dbc')) } ================================================ FILE: modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainer.java ================================================ package org.testcontainers.containers; import io.r2dbc.spi.ConnectionFactoryOptions; import lombok.RequiredArgsConstructor; import lombok.experimental.Delegate; import org.testcontainers.lifecycle.Startable; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; @RequiredArgsConstructor public class MSSQLR2DBCDatabaseContainer implements R2DBCDatabaseContainer { @Delegate(types = Startable.class) private final MSSQLServerContainer container; public static ConnectionFactoryOptions getOptions(MSSQLServerContainer container) { ConnectionFactoryOptions options = ConnectionFactoryOptions .builder() .option(ConnectionFactoryOptions.DRIVER, MSSQLR2DBCDatabaseContainerProvider.DRIVER) .build(); return new MSSQLR2DBCDatabaseContainer(container).configure(options); } @Override public ConnectionFactoryOptions configure(ConnectionFactoryOptions options) { return options .mutate() .option(ConnectionFactoryOptions.HOST, container.getHost()) .option(ConnectionFactoryOptions.PORT, container.getMappedPort(MSSQLServerContainer.MS_SQL_SERVER_PORT)) // TODO enable if/when MSSQLServerContainer adds support for customizing the DB name // .option(ConnectionFactoryOptions.DATABASE, container.getDatabasseName()) .option(ConnectionFactoryOptions.USER, container.getUsername()) .option(ConnectionFactoryOptions.PASSWORD, container.getPassword()) .build(); } } ================================================ FILE: modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerProvider.java ================================================ package org.testcontainers.containers; import io.r2dbc.mssql.MssqlConnectionFactoryProvider; import io.r2dbc.spi.ConnectionFactoryMetadata; import io.r2dbc.spi.ConnectionFactoryOptions; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; import org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider; import javax.annotation.Nullable; public class MSSQLR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider { static final String DRIVER = MssqlConnectionFactoryProvider.MSSQL_DRIVER; @Override public boolean supports(ConnectionFactoryOptions options) { return DRIVER.equals(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)); } @Override public R2DBCDatabaseContainer createContainer(ConnectionFactoryOptions options) { // TODO work out how best to do this if these constants become private String image = MSSQLServerContainer.IMAGE + ":" + options.getRequiredValue(IMAGE_TAG_OPTION); MSSQLServerContainer container = new MSSQLServerContainer<>(image); if (Boolean.TRUE.equals(options.getValue(REUSABLE_OPTION))) { container.withReuse(true); } return new MSSQLR2DBCDatabaseContainer(container); } @Nullable @Override public ConnectionFactoryMetadata getMetadata(ConnectionFactoryOptions options) { ConnectionFactoryOptions.Builder builder = options.mutate(); if (!options.hasOption(ConnectionFactoryOptions.USER)) { builder.option(ConnectionFactoryOptions.USER, MSSQLServerContainer.DEFAULT_USER); } if (!options.hasOption(ConnectionFactoryOptions.PASSWORD)) { builder.option(ConnectionFactoryOptions.PASSWORD, MSSQLServerContainer.DEFAULT_PASSWORD); } return R2DBCDatabaseContainerProvider.super.getMetadata(builder.build()); } } ================================================ FILE: modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainer.java ================================================ package org.testcontainers.containers; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.LicenseAcceptance; import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Stream; /** * Testcontainers implementation for Microsoft SQL Server. *

* Supported image: {@code mcr.microsoft.com/mssql/server} *

* Exposed ports: 1433 * * @deprecated use {@link org.testcontainers.mssqlserver.MSSQLServerContainer} instead. */ @Deprecated public class MSSQLServerContainer> extends JdbcDatabaseContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("mcr.microsoft.com/mssql/server"); @Deprecated public static final String DEFAULT_TAG = "2017-CU12"; public static final String NAME = "sqlserver"; public static final String IMAGE = DEFAULT_IMAGE_NAME.getUnversionedPart(); public static final Integer MS_SQL_SERVER_PORT = 1433; static final String DEFAULT_USER = "sa"; static final String DEFAULT_PASSWORD = "A_Str0ng_Required_Password"; private String password = DEFAULT_PASSWORD; private static final int DEFAULT_STARTUP_TIMEOUT_SECONDS = 240; private static final int DEFAULT_CONNECT_TIMEOUT_SECONDS = 240; private static final Pattern[] PASSWORD_CATEGORY_VALIDATION_PATTERNS = new Pattern[] { Pattern.compile("[A-Z]+"), Pattern.compile("[a-z]+"), Pattern.compile("[0-9]+"), Pattern.compile("[^a-zA-Z0-9]+", Pattern.CASE_INSENSITIVE), }; /** * @deprecated use {@link #MSSQLServerContainer(DockerImageName)} instead */ @Deprecated public MSSQLServerContainer() { this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } public MSSQLServerContainer(final String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public MSSQLServerContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); withStartupTimeoutSeconds(DEFAULT_STARTUP_TIMEOUT_SECONDS); withConnectTimeoutSeconds(DEFAULT_CONNECT_TIMEOUT_SECONDS); addExposedPort(MS_SQL_SERVER_PORT); } @Override public Set getLivenessCheckPortNumbers() { return super.getLivenessCheckPortNumbers(); } @Override protected void configure() { // If license was not accepted programmatically, check if it was accepted via resource file if (!getEnvMap().containsKey("ACCEPT_EULA")) { LicenseAcceptance.assertLicenseAccepted(this.getDockerImageName()); acceptLicense(); } addEnv("MSSQL_SA_PASSWORD", password); } /** * Accepts the license for the SQLServer container by setting the ACCEPT_EULA=Y * variable as described at https://hub.docker.com/_/microsoft-mssql-server */ public SELF acceptLicense() { addEnv("ACCEPT_EULA", "Y"); return self(); } @Override public String getDriverClassName() { return "com.microsoft.sqlserver.jdbc.SQLServerDriver"; } @Override protected String constructUrlForConnection(String queryString) { // The JDBC driver of MS SQL Server enables encryption by default for versions > 10.1.0. // We need to disable it by default to be able to use the container without having to pass extra params. // See https://github.com/microsoft/mssql-jdbc/releases/tag/v10.1.0 if (urlParameters.keySet().stream().map(String::toLowerCase).noneMatch("encrypt"::equals)) { urlParameters.put("encrypt", "false"); } return super.constructUrlForConnection(queryString); } @Override public String getJdbcUrl() { String additionalUrlParams = constructUrlParameters(";", ";"); return "jdbc:sqlserver://" + getHost() + ":" + getMappedPort(MS_SQL_SERVER_PORT) + additionalUrlParams; } @Override public String getUsername() { return DEFAULT_USER; } @Override public String getPassword() { return password; } @Override public String getTestQueryString() { return "SELECT 1"; } @Override public SELF withPassword(final String password) { checkPasswordStrength(password); this.password = password; return self(); } private void checkPasswordStrength(String password) { if (password == null) { throw new IllegalArgumentException("Null password is not allowed"); } if (password.length() < 8) { throw new IllegalArgumentException("Password should be at least 8 characters long"); } if (password.length() > 128) { throw new IllegalArgumentException("Password can be up to 128 characters long"); } long satisfiedCategories = Stream .of(PASSWORD_CATEGORY_VALIDATION_PATTERNS) .filter(p -> p.matcher(password).find()) .count(); if (satisfiedCategories < 3) { throw new IllegalArgumentException( "Password must contain characters from three of the following four categories:\n" + " - Latin uppercase letters (A through Z)\n" + " - Latin lowercase letters (a through z)\n" + " - Base 10 digits (0 through 9)\n" + " - Non-alphanumeric characters such as: exclamation point (!), dollar sign ($), number sign (#), " + "or percent (%)." ); } } } ================================================ FILE: modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainerProvider.java ================================================ package org.testcontainers.containers; import org.testcontainers.utility.DockerImageName; /** * Factory for MS SQL Server containers. */ public class MSSQLServerContainerProvider extends JdbcDatabaseContainerProvider { @Override public boolean supports(String databaseType) { return databaseType.equals(MSSQLServerContainer.NAME); } @Override public JdbcDatabaseContainer newInstance() { return newInstance(MSSQLServerContainer.DEFAULT_TAG); } @Override public JdbcDatabaseContainer newInstance(String tag) { return new MSSQLServerContainer(DockerImageName.parse(MSSQLServerContainer.IMAGE).withTag(tag)); } } ================================================ FILE: modules/mssqlserver/src/main/java/org/testcontainers/mssqlserver/MSSQLR2DBCDatabaseContainer.java ================================================ package org.testcontainers.mssqlserver; import io.r2dbc.mssql.MssqlConnectionFactoryProvider; import io.r2dbc.spi.ConnectionFactoryOptions; import org.testcontainers.lifecycle.Startable; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; import java.util.Set; public class MSSQLR2DBCDatabaseContainer implements R2DBCDatabaseContainer { private final MSSQLServerContainer container; public MSSQLR2DBCDatabaseContainer(MSSQLServerContainer container) { this.container = container; } public static ConnectionFactoryOptions getOptions(MSSQLServerContainer container) { ConnectionFactoryOptions options = ConnectionFactoryOptions .builder() .option(ConnectionFactoryOptions.DRIVER, MssqlConnectionFactoryProvider.MSSQL_DRIVER) .build(); return new MSSQLR2DBCDatabaseContainer(container).configure(options); } @Override public ConnectionFactoryOptions configure(ConnectionFactoryOptions options) { return options .mutate() .option(ConnectionFactoryOptions.HOST, container.getHost()) .option(ConnectionFactoryOptions.PORT, container.getMappedPort(MSSQLServerContainer.MS_SQL_SERVER_PORT)) // TODO enable if/when MSSQLServerContainer adds support for customizing the DB name // .option(ConnectionFactoryOptions.DATABASE, container.getDatabasseName()) .option(ConnectionFactoryOptions.USER, container.getUsername()) .option(ConnectionFactoryOptions.PASSWORD, container.getPassword()) .build(); } @Override public Set getDependencies() { return this.container.getDependencies(); } @Override public void start() { this.container.start(); } @Override public void stop() { this.container.stop(); } @Override public void close() { this.container.close(); } } ================================================ FILE: modules/mssqlserver/src/main/java/org/testcontainers/mssqlserver/MSSQLServerContainer.java ================================================ package org.testcontainers.mssqlserver; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.LicenseAcceptance; import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Stream; /** * Testcontainers implementation for Microsoft SQL Server. *

* Supported image: {@code mcr.microsoft.com/mssql/server} *

* Exposed ports: 1433 */ public class MSSQLServerContainer extends JdbcDatabaseContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("mcr.microsoft.com/mssql/server"); public static final String NAME = "sqlserver"; public static final String IMAGE = DEFAULT_IMAGE_NAME.getUnversionedPart(); public static final Integer MS_SQL_SERVER_PORT = 1433; static final String DEFAULT_USER = "sa"; static final String DEFAULT_PASSWORD = "A_Str0ng_Required_Password"; private String password = DEFAULT_PASSWORD; private static final int DEFAULT_STARTUP_TIMEOUT_SECONDS = 240; private static final int DEFAULT_CONNECT_TIMEOUT_SECONDS = 240; private static final Pattern[] PASSWORD_CATEGORY_VALIDATION_PATTERNS = new Pattern[] { Pattern.compile("[A-Z]+"), Pattern.compile("[a-z]+"), Pattern.compile("[0-9]+"), Pattern.compile("[^a-zA-Z0-9]+", Pattern.CASE_INSENSITIVE), }; public MSSQLServerContainer(final String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public MSSQLServerContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); withStartupTimeoutSeconds(DEFAULT_STARTUP_TIMEOUT_SECONDS); withConnectTimeoutSeconds(DEFAULT_CONNECT_TIMEOUT_SECONDS); addExposedPort(MS_SQL_SERVER_PORT); } @Override public Set getLivenessCheckPortNumbers() { return super.getLivenessCheckPortNumbers(); } @Override protected void configure() { // If license was not accepted programmatically, check if it was accepted via resource file if (!getEnvMap().containsKey("ACCEPT_EULA")) { LicenseAcceptance.assertLicenseAccepted(this.getDockerImageName()); acceptLicense(); } addEnv("MSSQL_SA_PASSWORD", password); } /** * Accepts the license for the SQLServer container by setting the ACCEPT_EULA=Y * variable as described at https://hub.docker.com/_/microsoft-mssql-server */ public MSSQLServerContainer acceptLicense() { addEnv("ACCEPT_EULA", "Y"); return self(); } @Override public String getDriverClassName() { return "com.microsoft.sqlserver.jdbc.SQLServerDriver"; } @Override protected String constructUrlForConnection(String queryString) { // The JDBC driver of MS SQL Server enables encryption by default for versions > 10.1.0. // We need to disable it by default to be able to use the container without having to pass extra params. // See https://github.com/microsoft/mssql-jdbc/releases/tag/v10.1.0 if (urlParameters.keySet().stream().map(String::toLowerCase).noneMatch("encrypt"::equals)) { urlParameters.put("encrypt", "false"); } return super.constructUrlForConnection(queryString); } @Override public String getJdbcUrl() { String additionalUrlParams = constructUrlParameters(";", ";"); return "jdbc:sqlserver://" + getHost() + ":" + getMappedPort(MS_SQL_SERVER_PORT) + additionalUrlParams; } @Override public String getUsername() { return DEFAULT_USER; } @Override public String getPassword() { return password; } @Override public String getTestQueryString() { return "SELECT 1"; } @Override public MSSQLServerContainer withPassword(final String password) { checkPasswordStrength(password); this.password = password; return self(); } private void checkPasswordStrength(String password) { if (password == null) { throw new IllegalArgumentException("Null password is not allowed"); } if (password.length() < 8) { throw new IllegalArgumentException("Password should be at least 8 characters long"); } if (password.length() > 128) { throw new IllegalArgumentException("Password can be up to 128 characters long"); } long satisfiedCategories = Stream .of(PASSWORD_CATEGORY_VALIDATION_PATTERNS) .filter(p -> p.matcher(password).find()) .count(); if (satisfiedCategories < 3) { throw new IllegalArgumentException( "Password must contain characters from three of the following four categories:\n" + " - Latin uppercase letters (A through Z)\n" + " - Latin lowercase letters (a through z)\n" + " - Base 10 digits (0 through 9)\n" + " - Non-alphanumeric characters such as: exclamation point (!), dollar sign ($), number sign (#), " + "or percent (%)." ); } } } ================================================ FILE: modules/mssqlserver/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider ================================================ org.testcontainers.containers.MSSQLServerContainerProvider ================================================ FILE: modules/mssqlserver/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider ================================================ org.testcontainers.containers.MSSQLR2DBCDatabaseContainerProvider ================================================ FILE: modules/mssqlserver/src/test/java/org/testcontainers/MSSQLServerTestImages.java ================================================ package org.testcontainers; import org.testcontainers.utility.DockerImageName; public interface MSSQLServerTestImages { DockerImageName MSSQL_SERVER_IMAGE = DockerImageName.parse("mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04"); } ================================================ FILE: modules/mssqlserver/src/test/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerTest.java ================================================ package org.testcontainers.containers; import io.r2dbc.spi.ConnectionFactoryOptions; import org.testcontainers.MSSQLServerTestImages; import org.testcontainers.r2dbc.AbstractR2DBCDatabaseContainerTest; public class MSSQLR2DBCDatabaseContainerTest extends AbstractR2DBCDatabaseContainerTest> { @Override protected ConnectionFactoryOptions getOptions(MSSQLServerContainer container) { return MSSQLR2DBCDatabaseContainer.getOptions(container); } @Override protected String createR2DBCUrl() { return "r2dbc:tc:sqlserver:///?TC_IMAGE_TAG=2022-CU14-ubuntu-22.04"; } @Override protected MSSQLServerContainer createContainer() { return new MSSQLServerContainer<>(MSSQLServerTestImages.MSSQL_SERVER_IMAGE); } } ================================================ FILE: modules/mssqlserver/src/test/java/org/testcontainers/jdbc/mssqlserver/MSSQLServerJDBCDriverTest.java ================================================ package org.testcontainers.jdbc.mssqlserver; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; class MSSQLServerJDBCDriverTest extends AbstractJDBCDriverTest { public static Iterable data() { return Arrays.asList( new Object[][] { { "jdbc:tc:sqlserver:2022-CU14-ubuntu-22.04://hostname:hostport;databaseName=databasename", EnumSet.noneOf(Options.class), }, } ); } } ================================================ FILE: modules/mssqlserver/src/test/java/org/testcontainers/mssqlserver/CustomPasswordMSSQLServerTest.java ================================================ package org.testcontainers.mssqlserver; import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.testcontainers.MSSQLServerTestImages; import org.testcontainers.containers.MSSQLServerContainer; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.fail; /** * Tests if the password passed to the container satisfied the password policy described at * https://docs.microsoft.com/en-us/sql/relational-databases/security/password-policy?view=sql-server-2017 */ public class CustomPasswordMSSQLServerTest { private static String UPPER_CASE_LETTERS = "ABCDE"; private static String LOWER_CASE_LETTERS = "abcde"; private static String NUMBERS = "12345"; private static String SPECIAL_CHARS = "_(!)_"; public static Stream data() { return Stream.of( Arguments.arguments(null, false), // too short Arguments.arguments("abc123", false), // too long Arguments.arguments(RandomStringUtils.randomAlphabetic(129), false), // only 2 categories Arguments.arguments(UPPER_CASE_LETTERS + NUMBERS, false), Arguments.arguments(UPPER_CASE_LETTERS + SPECIAL_CHARS, false), Arguments.arguments(LOWER_CASE_LETTERS + NUMBERS, false), Arguments.arguments(LOWER_CASE_LETTERS + SPECIAL_CHARS, false), Arguments.arguments(NUMBERS + SPECIAL_CHARS, false), // 3 categories Arguments.arguments(UPPER_CASE_LETTERS + LOWER_CASE_LETTERS + NUMBERS, true), Arguments.arguments(UPPER_CASE_LETTERS + LOWER_CASE_LETTERS + SPECIAL_CHARS, true), Arguments.arguments(UPPER_CASE_LETTERS + NUMBERS + SPECIAL_CHARS, true), Arguments.arguments(LOWER_CASE_LETTERS + NUMBERS + SPECIAL_CHARS, true), // 4 categories Arguments.arguments(UPPER_CASE_LETTERS + LOWER_CASE_LETTERS + NUMBERS + SPECIAL_CHARS, true) ); } @ParameterizedTest @MethodSource("data") public void runPasswordTests(String password, boolean valid) { try { new MSSQLServerContainer<>(MSSQLServerTestImages.MSSQL_SERVER_IMAGE).withPassword(password); if (!valid) { fail("Password " + password + " is not valid. Expected exception"); } } catch (IllegalArgumentException e) { if (valid) { fail("Password " + password + " should have been validated"); } } } } ================================================ FILE: modules/mssqlserver/src/test/java/org/testcontainers/mssqlserver/MSSQLR2DBCDatabaseContainerTest.java ================================================ package org.testcontainers.mssqlserver; import io.r2dbc.spi.ConnectionFactoryOptions; import org.testcontainers.MSSQLServerTestImages; import org.testcontainers.r2dbc.AbstractR2DBCDatabaseContainerTest; class MSSQLR2DBCDatabaseContainerTest extends AbstractR2DBCDatabaseContainerTest { @Override protected ConnectionFactoryOptions getOptions(MSSQLServerContainer container) { return MSSQLR2DBCDatabaseContainer.getOptions(container); } @Override protected String createR2DBCUrl() { return "r2dbc:tc:sqlserver:///?TC_IMAGE_TAG=2022-CU14-ubuntu-22.04"; } @Override protected MSSQLServerContainer createContainer() { return new MSSQLServerContainer(MSSQLServerTestImages.MSSQL_SERVER_IMAGE); } } ================================================ FILE: modules/mssqlserver/src/test/java/org/testcontainers/mssqlserver/MSSQLServerContainerTest.java ================================================ package org.testcontainers.mssqlserver; import org.junit.jupiter.api.Test; import org.testcontainers.MSSQLServerTestImages; import org.testcontainers.db.AbstractContainerDatabaseTest; import org.testcontainers.utility.DockerImageName; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import javax.sql.DataSource; import static org.assertj.core.api.Assertions.assertThat; class MSSQLServerContainerTest extends AbstractContainerDatabaseTest { @Test void testSimple() throws SQLException { try ( // container { MSSQLServerContainer mssqlServer = new MSSQLServerContainer( "mcr.microsoft.com/mssql/server:2022-CU20-ubuntu-22.04" ) .acceptLicense() // } ) { mssqlServer.start(); ResultSet resultSet = performQuery(mssqlServer, "SELECT 1"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); assertHasCorrectExposedAndLivenessCheckPorts(mssqlServer); } } @Test void testWithAdditionalUrlParamInJdbcUrl() { try ( MSSQLServerContainer mssqlServer = new MSSQLServerContainer(MSSQLServerTestImages.MSSQL_SERVER_IMAGE) .withUrlParam("integratedSecurity", "false") .withUrlParam("applicationName", "MyApp") ) { mssqlServer.start(); String jdbcUrl = mssqlServer.getJdbcUrl(); assertThat(jdbcUrl).contains(";integratedSecurity=false;applicationName=MyApp"); } } @Test void testSetupDatabase() throws SQLException { try (MSSQLServerContainer mssqlServer = new MSSQLServerContainer(MSSQLServerTestImages.MSSQL_SERVER_IMAGE)) { mssqlServer.start(); DataSource ds = getDataSource(mssqlServer); Statement statement = ds.getConnection().createStatement(); statement.executeUpdate("CREATE DATABASE [test];"); statement = ds.getConnection().createStatement(); statement.executeUpdate("CREATE TABLE [test].[dbo].[Foo](ID INT PRIMARY KEY);"); statement = ds.getConnection().createStatement(); statement.executeUpdate("INSERT INTO [test].[dbo].[Foo] (ID) VALUES (3);"); statement = ds.getConnection().createStatement(); statement.execute("SELECT * FROM [test].[dbo].[Foo];"); ResultSet resultSet = statement.getResultSet(); resultSet.next(); int resultSetInt = resultSet.getInt("ID"); assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(3); } } @Test void testSqlServerConnection() throws SQLException { try ( MSSQLServerContainer mssqlServerContainer = new MSSQLServerContainer( DockerImageName.parse("mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04") ) .withPassword("myStrong(!)Password") ) { mssqlServerContainer.start(); ResultSet resultSet = performQuery(mssqlServerContainer, mssqlServerContainer.getTestQueryString()); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); } } private void assertHasCorrectExposedAndLivenessCheckPorts(MSSQLServerContainer mssqlServer) { assertThat(mssqlServer.getExposedPorts()).containsExactly(MSSQLServerContainer.MS_SQL_SERVER_PORT); assertThat(mssqlServer.getLivenessCheckPortNumbers()) .containsExactly(mssqlServer.getMappedPort(MSSQLServerContainer.MS_SQL_SERVER_PORT)); } } ================================================ FILE: modules/mssqlserver/src/test/resources/container-license-acceptance.txt ================================================ mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04 ================================================ FILE: modules/mssqlserver/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/mysql/build.gradle ================================================ description = "Testcontainers :: JDBC :: MySQL" dependencies { api project(':testcontainers-jdbc') compileOnly project(':testcontainers-r2dbc') compileOnly 'io.asyncer:r2dbc-mysql:1.4.1' testImplementation project(':testcontainers-jdbc-test') testRuntimeOnly 'com.mysql:mysql-connector-j:9.5.0' testImplementation testFixtures(project(':testcontainers-r2dbc')) testRuntimeOnly 'io.asyncer:r2dbc-mysql:1.4.1' compileOnly 'org.jetbrains:annotations:26.0.2-1' } ================================================ FILE: modules/mysql/sql/init_mysql.sql ================================================ CREATE TABLE bar ( foo VARCHAR(255) ); INSERT INTO bar (foo) VALUES ('hello world'); ================================================ FILE: modules/mysql/src/main/java/org/testcontainers/containers/MySQLContainer.java ================================================ package org.testcontainers.containers; import org.jetbrains.annotations.NotNull; import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.DockerImageName; import java.util.Set; /** * Testcontainers implementation for MySQL. *

* Supported image: {@code mysql} *

* Exposed ports: 3306 * * @deprecated use {@link org.testcontainers.mysql.MySQLContainer} instead. */ @Deprecated public class MySQLContainer> extends JdbcDatabaseContainer { public static final String NAME = "mysql"; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("mysql"); @Deprecated public static final String DEFAULT_TAG = "5.7.34"; @Deprecated public static final String IMAGE = DEFAULT_IMAGE_NAME.getUnversionedPart(); static final String DEFAULT_USER = "test"; static final String DEFAULT_PASSWORD = "test"; private static final String MY_CNF_CONFIG_OVERRIDE_PARAM_NAME = "TC_MY_CNF"; public static final Integer MYSQL_PORT = 3306; private String databaseName = "test"; private String username = DEFAULT_USER; private String password = DEFAULT_PASSWORD; private static final String MYSQL_ROOT_USER = "root"; /** * @deprecated use {@link #MySQLContainer(DockerImageName)} instead */ @Deprecated public MySQLContainer() { this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } public MySQLContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public MySQLContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); addExposedPort(MYSQL_PORT); } /** * @return the ports on which to check if the container is ready * @deprecated use {@link #getLivenessCheckPortNumbers()} instead */ @NotNull @Override @Deprecated protected Set getLivenessCheckPorts() { return super.getLivenessCheckPorts(); } @Override protected void configure() { optionallyMapResourceParameterAsVolume( MY_CNF_CONFIG_OVERRIDE_PARAM_NAME, "/etc/mysql/conf.d", "mysql-default-conf", Transferable.DEFAULT_DIR_MODE ); addEnv("MYSQL_DATABASE", databaseName); if (!MYSQL_ROOT_USER.equalsIgnoreCase(username)) { addEnv("MYSQL_USER", username); } if (password != null && !password.isEmpty()) { addEnv("MYSQL_PASSWORD", password); addEnv("MYSQL_ROOT_PASSWORD", password); } else if (MYSQL_ROOT_USER.equalsIgnoreCase(username)) { addEnv("MYSQL_ALLOW_EMPTY_PASSWORD", "yes"); } else { throw new ContainerLaunchException("Empty password can be used only with the root user"); } setStartupAttempts(3); } @Override public String getDriverClassName() { try { Class.forName("com.mysql.cj.jdbc.Driver"); return "com.mysql.cj.jdbc.Driver"; } catch (ClassNotFoundException e) { return "com.mysql.jdbc.Driver"; } } @Override public String getJdbcUrl() { String additionalUrlParams = constructUrlParameters("?", "&"); return "jdbc:mysql://" + getHost() + ":" + getMappedPort(MYSQL_PORT) + "/" + databaseName + additionalUrlParams; } @Override protected String constructUrlForConnection(String queryString) { String url = super.constructUrlForConnection(queryString); if (!url.contains("useSSL=")) { String separator = url.contains("?") ? "&" : "?"; url = url + separator + "useSSL=false"; } if (!url.contains("allowPublicKeyRetrieval=")) { url = url + "&allowPublicKeyRetrieval=true"; } return url; } @Override public String getDatabaseName() { return databaseName; } @Override public String getUsername() { return username; } @Override public String getPassword() { return password; } @Override public String getTestQueryString() { return "SELECT 1"; } public SELF withConfigurationOverride(String s) { parameters.put(MY_CNF_CONFIG_OVERRIDE_PARAM_NAME, s); return self(); } @Override public SELF withDatabaseName(final String databaseName) { this.databaseName = databaseName; return self(); } @Override public SELF withUsername(final String username) { this.username = username; return self(); } @Override public SELF withPassword(final String password) { this.password = password; return self(); } } ================================================ FILE: modules/mysql/src/main/java/org/testcontainers/containers/MySQLContainerProvider.java ================================================ package org.testcontainers.containers; import org.testcontainers.jdbc.ConnectionUrl; import org.testcontainers.utility.DockerImageName; /** * Factory for MySQL containers. */ public class MySQLContainerProvider extends JdbcDatabaseContainerProvider { private static final String USER_PARAM = "user"; private static final String PASSWORD_PARAM = "password"; @Override public boolean supports(String databaseType) { return databaseType.equals(MySQLContainer.NAME); } @Override public JdbcDatabaseContainer newInstance() { return newInstance(MySQLContainer.DEFAULT_TAG); } @Override public JdbcDatabaseContainer newInstance(String tag) { if (tag != null) { return new MySQLContainer(DockerImageName.parse(MySQLContainer.IMAGE).withTag(tag)); } else { return newInstance(); } } @Override public JdbcDatabaseContainer newInstance(ConnectionUrl connectionUrl) { return newInstanceFromConnectionUrl(connectionUrl, USER_PARAM, PASSWORD_PARAM); } } ================================================ FILE: modules/mysql/src/main/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainer.java ================================================ package org.testcontainers.containers; import io.r2dbc.spi.ConnectionFactoryOptions; import lombok.RequiredArgsConstructor; import lombok.experimental.Delegate; import org.testcontainers.lifecycle.Startable; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; @RequiredArgsConstructor public class MySQLR2DBCDatabaseContainer implements R2DBCDatabaseContainer { @Delegate(types = Startable.class) private final MySQLContainer container; public static ConnectionFactoryOptions getOptions(MySQLContainer container) { ConnectionFactoryOptions options = ConnectionFactoryOptions .builder() .option(ConnectionFactoryOptions.DRIVER, MySQLR2DBCDatabaseContainerProvider.DRIVER) .build(); return new MySQLR2DBCDatabaseContainer(container).configure(options); } @Override public ConnectionFactoryOptions configure(ConnectionFactoryOptions options) { return options .mutate() .option(ConnectionFactoryOptions.HOST, container.getHost()) .option(ConnectionFactoryOptions.PORT, container.getMappedPort(MySQLContainer.MYSQL_PORT)) .option(ConnectionFactoryOptions.DATABASE, container.getDatabaseName()) .option(ConnectionFactoryOptions.USER, container.getUsername()) .option(ConnectionFactoryOptions.PASSWORD, container.getPassword()) .build(); } } ================================================ FILE: modules/mysql/src/main/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerProvider.java ================================================ package org.testcontainers.containers; import io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider; import io.r2dbc.spi.ConnectionFactoryMetadata; import io.r2dbc.spi.ConnectionFactoryOptions; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; import org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider; import javax.annotation.Nullable; public class MySQLR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider { static final String DRIVER = MySqlConnectionFactoryProvider.MYSQL_DRIVER; @Override public boolean supports(ConnectionFactoryOptions options) { return DRIVER.equals(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)); } @Override public R2DBCDatabaseContainer createContainer(ConnectionFactoryOptions options) { String image = MySQLContainer.IMAGE + ":" + options.getRequiredValue(IMAGE_TAG_OPTION); MySQLContainer container = new MySQLContainer<>(image) .withDatabaseName((String) options.getRequiredValue(ConnectionFactoryOptions.DATABASE)); if (Boolean.TRUE.equals(options.getValue(REUSABLE_OPTION))) { container.withReuse(true); } return new MySQLR2DBCDatabaseContainer(container); } @Nullable @Override public ConnectionFactoryMetadata getMetadata(ConnectionFactoryOptions options) { ConnectionFactoryOptions.Builder builder = options.mutate(); if (!options.hasOption(ConnectionFactoryOptions.USER)) { builder.option(ConnectionFactoryOptions.USER, MySQLContainer.DEFAULT_USER); } if (!options.hasOption(ConnectionFactoryOptions.PASSWORD)) { builder.option(ConnectionFactoryOptions.PASSWORD, MySQLContainer.DEFAULT_PASSWORD); } return R2DBCDatabaseContainerProvider.super.getMetadata(builder.build()); } } ================================================ FILE: modules/mysql/src/main/java/org/testcontainers/mysql/MySQLContainer.java ================================================ package org.testcontainers.mysql; import org.jetbrains.annotations.NotNull; import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.DockerImageName; import java.util.Set; /** * Testcontainers implementation for MySQL. *

* Supported image: {@code mysql} *

* Exposed ports: 3306 */ public class MySQLContainer extends JdbcDatabaseContainer { public static final String NAME = "mysql"; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("mysql"); static final String DEFAULT_USER = "test"; static final String DEFAULT_PASSWORD = "test"; private static final String MY_CNF_CONFIG_OVERRIDE_PARAM_NAME = "TC_MY_CNF"; public static final Integer MYSQL_PORT = 3306; private String databaseName = "test"; private String username = DEFAULT_USER; private String password = DEFAULT_PASSWORD; private static final String MYSQL_ROOT_USER = "root"; public MySQLContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public MySQLContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); addExposedPort(MYSQL_PORT); } /** * @return the ports on which to check if the container is ready * @deprecated use {@link #getLivenessCheckPortNumbers()} instead */ @NotNull @Override @Deprecated protected Set getLivenessCheckPorts() { return super.getLivenessCheckPorts(); } @Override protected void configure() { optionallyMapResourceParameterAsVolume( MY_CNF_CONFIG_OVERRIDE_PARAM_NAME, "/etc/mysql/conf.d", null, Transferable.DEFAULT_DIR_MODE ); addEnv("MYSQL_DATABASE", databaseName); if (!MYSQL_ROOT_USER.equalsIgnoreCase(username)) { addEnv("MYSQL_USER", username); } if (password != null && !password.isEmpty()) { addEnv("MYSQL_PASSWORD", password); addEnv("MYSQL_ROOT_PASSWORD", password); } else if (MYSQL_ROOT_USER.equalsIgnoreCase(username)) { addEnv("MYSQL_ALLOW_EMPTY_PASSWORD", "yes"); } else { throw new ContainerLaunchException("Empty password can be used only with the root user"); } setStartupAttempts(3); } @Override public String getDriverClassName() { try { Class.forName("com.mysql.cj.jdbc.Driver"); return "com.mysql.cj.jdbc.Driver"; } catch (ClassNotFoundException e) { return "com.mysql.jdbc.Driver"; } } @Override public String getJdbcUrl() { String additionalUrlParams = constructUrlParameters("?", "&"); return "jdbc:mysql://" + getHost() + ":" + getMappedPort(MYSQL_PORT) + "/" + databaseName + additionalUrlParams; } @Override protected String constructUrlForConnection(String queryString) { String url = super.constructUrlForConnection(queryString); if (!url.contains("useSSL=")) { String separator = url.contains("?") ? "&" : "?"; url = url + separator + "useSSL=false"; } if (!url.contains("allowPublicKeyRetrieval=")) { url = url + "&allowPublicKeyRetrieval=true"; } return url; } @Override public String getDatabaseName() { return databaseName; } @Override public String getUsername() { return username; } @Override public String getPassword() { return password; } @Override public String getTestQueryString() { return "SELECT 1"; } public MySQLContainer withConfigurationOverride(String s) { parameters.put(MY_CNF_CONFIG_OVERRIDE_PARAM_NAME, s); return self(); } @Override public MySQLContainer withDatabaseName(final String databaseName) { this.databaseName = databaseName; return self(); } @Override public MySQLContainer withUsername(final String username) { this.username = username; return self(); } @Override public MySQLContainer withPassword(final String password) { this.password = password; return self(); } } ================================================ FILE: modules/mysql/src/main/java/org/testcontainers/mysql/MySQLR2DBCDatabaseContainer.java ================================================ package org.testcontainers.mysql; import io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider; import io.r2dbc.spi.ConnectionFactoryOptions; import org.testcontainers.lifecycle.Startable; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; import java.util.Set; public class MySQLR2DBCDatabaseContainer implements R2DBCDatabaseContainer { private final MySQLContainer container; public MySQLR2DBCDatabaseContainer(MySQLContainer container) { this.container = container; } public static ConnectionFactoryOptions getOptions(MySQLContainer container) { ConnectionFactoryOptions options = ConnectionFactoryOptions .builder() .option(ConnectionFactoryOptions.DRIVER, MySqlConnectionFactoryProvider.MYSQL_DRIVER) .build(); return new MySQLR2DBCDatabaseContainer(container).configure(options); } @Override public ConnectionFactoryOptions configure(ConnectionFactoryOptions options) { return options .mutate() .option(ConnectionFactoryOptions.HOST, container.getHost()) .option(ConnectionFactoryOptions.PORT, container.getMappedPort(MySQLContainer.MYSQL_PORT)) .option(ConnectionFactoryOptions.DATABASE, container.getDatabaseName()) .option(ConnectionFactoryOptions.USER, container.getUsername()) .option(ConnectionFactoryOptions.PASSWORD, container.getPassword()) .build(); } @Override public Set getDependencies() { return this.container.getDependencies(); } @Override public void start() { this.container.start(); } @Override public void stop() { this.container.stop(); } @Override public void close() { this.container.close(); } } ================================================ FILE: modules/mysql/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider ================================================ org.testcontainers.containers.MySQLContainerProvider ================================================ FILE: modules/mysql/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider ================================================ org.testcontainers.containers.MySQLR2DBCDatabaseContainerProvider ================================================ FILE: modules/mysql/src/main/resources/mysql-default-conf/my.cnf ================================================ [mysqld] user = mysql datadir = /var/lib/mysql port = 3306 #socket = /tmp/mysql.sock skip-external-locking key_buffer_size = 16K max_allowed_packet = 1M table_open_cache = 4 sort_buffer_size = 64K read_buffer_size = 256K read_rnd_buffer_size = 256K net_buffer_length = 2K host_cache_size = 0 skip-name-resolve # Don't listen on a TCP/IP port at all. This can be a security enhancement, # if all processes that need to connect to mysqld run on the same host. # All interaction with mysqld must be made via Unix sockets or named pipes. # Note that using this option without enabling named pipes on Windows # (using the "enable-named-pipe" option) will render mysqld useless! # #skip-networking #server-id = 1 # Uncomment the following if you want to log updates #log-bin=mysql-bin # binary logging format - mixed recommended #binlog_format=mixed # Causes updates to non-transactional engines using statement format to be # written directly to binary log. Before using this option make sure that # there are no dependencies between transactional and non-transactional # tables such as in the statement INSERT INTO t_myisam SELECT * FROM # t_innodb; otherwise, slaves may diverge from the master. #binlog_direct_non_transactional_updates=TRUE # Uncomment the following if you are using InnoDB tables innodb_data_file_path = ibdata1:10M:autoextend # You can set .._buffer_pool_size up to 50 - 80 % # of RAM but beware of setting memory usage too high innodb_buffer_pool_size = 16M #innodb_additional_mem_pool_size = 2M # Set .._log_file_size to 25 % of buffer pool size innodb_log_buffer_size = 8M innodb_flush_log_at_trx_commit = 1 innodb_lock_wait_timeout = 50 ================================================ FILE: modules/mysql/src/test/java/org/testcontainers/MySQLTestImages.java ================================================ package org.testcontainers; import org.testcontainers.utility.DockerImageName; public class MySQLTestImages { public static final DockerImageName MYSQL_57_IMAGE = DockerImageName.parse("mysql:5.7.44"); public static final DockerImageName MYSQL_80_IMAGE = DockerImageName.parse("mysql:8.0.36"); public static final DockerImageName MYSQL_INNOVATION_IMAGE = DockerImageName.parse("mysql:8.3.0"); public static final DockerImageName MYSQL_93_IMAGE = DockerImageName.parse("mysql:9.3.0"); } ================================================ FILE: modules/mysql/src/test/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerTest.java ================================================ package org.testcontainers.containers; import io.r2dbc.spi.ConnectionFactoryOptions; import org.testcontainers.MySQLTestImages; import org.testcontainers.r2dbc.AbstractR2DBCDatabaseContainerTest; public class MySQLR2DBCDatabaseContainerTest extends AbstractR2DBCDatabaseContainerTest> { @Override protected ConnectionFactoryOptions getOptions(MySQLContainer container) { return MySQLR2DBCDatabaseContainer.getOptions(container); } @Override protected String createR2DBCUrl() { return "r2dbc:tc:mysql:///db?TC_IMAGE_TAG=" + MySQLTestImages.MYSQL_80_IMAGE.getVersionPart(); } @Override protected MySQLContainer createContainer() { return new MySQLContainer<>(MySQLTestImages.MYSQL_80_IMAGE); } } ================================================ FILE: modules/mysql/src/test/java/org/testcontainers/containers/MySQLRootAccountTest.java ================================================ package org.testcontainers.containers; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.testcontainers.MySQLTestImages; import org.testcontainers.containers.output.Slf4jLogConsumer; import org.testcontainers.utility.DockerImageName; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; @Slf4j class MySQLRootAccountTest { public static DockerImageName[] params() { return new DockerImageName[] { MySQLTestImages.MYSQL_57_IMAGE, MySQLTestImages.MYSQL_80_IMAGE, MySQLTestImages.MYSQL_INNOVATION_IMAGE, MySQLTestImages.MYSQL_93_IMAGE, }; } @ParameterizedTest @MethodSource("params") void testRootAccountUsageWithDefaultPassword(DockerImageName image) throws SQLException { testWithDB(new MySQLContainer<>(image).withUsername("root")); } @ParameterizedTest @MethodSource("params") void testRootAccountUsageWithEmptyPassword(DockerImageName image) throws SQLException { testWithDB(new MySQLContainer<>(image).withUsername("root").withPassword("")); } @ParameterizedTest @MethodSource("params") void testRootAccountUsageWithCustomPassword(DockerImageName image) throws SQLException { testWithDB(new MySQLContainer<>(image).withUsername("root").withPassword("not-default")); } private void testWithDB(MySQLContainer db) throws SQLException { try { db.withLogConsumer(new Slf4jLogConsumer(log)).start(); Connection connection = DriverManager.getConnection(db.getJdbcUrl(), db.getUsername(), db.getPassword()); connection.createStatement().execute("SELECT 1"); connection.createStatement().execute("set sql_log_bin=0"); // requires root } finally { db.close(); } } } ================================================ FILE: modules/mysql/src/test/java/org/testcontainers/jdbc/mysql/JDBCDriverWithPoolTest.java ================================================ package org.testcontainers.jdbc.mysql; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.ResultSetHandler; import org.apache.tomcat.jdbc.pool.PoolProperties; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedClass; import org.junit.jupiter.params.provider.MethodSource; import org.testcontainers.jdbc.ContainerDatabaseDriver; import org.vibur.dbcp.ViburDBCPDataSource; import java.sql.Connection; import java.sql.SQLException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import java.util.stream.Stream; import javax.sql.DataSource; import static org.assertj.core.api.Assertions.assertThat; /** * This test belongs in the jdbc module, as it is focused on testing the behaviour of {@link org.testcontainers.containers.JdbcDatabaseContainer}. * However, the need to use the {@link org.testcontainers.containers.MySQLContainerProvider} (due to the jdbc:tc:mysql) URL forces it to live here in * the mysql module, to avoid circular dependencies. * TODO: Move to the jdbc module and either (a) implement a barebones {@link org.testcontainers.containers.JdbcDatabaseContainerProvider} for testing, or (b) refactor into a unit test. */ @ParameterizedClass @MethodSource("dataSourceSuppliers") public class JDBCDriverWithPoolTest { public static final String URL = "jdbc:tc:mysql:8.0.36://hostname/databasename?TC_INITFUNCTION=org.testcontainers.jdbc.mysql.JDBCDriverWithPoolTest::sampleInitFunction"; private final DataSource dataSource; public static Stream> dataSourceSuppliers() { return Stream.of( JDBCDriverWithPoolTest::getTomcatDataSourceWithDriverClassName, JDBCDriverWithPoolTest::getTomcatDataSource, JDBCDriverWithPoolTest::getHikariDataSourceWithDriverClassName, JDBCDriverWithPoolTest::getHikariDataSource, JDBCDriverWithPoolTest::getViburDataSourceWithDriverClassName, JDBCDriverWithPoolTest::getViburDataSource ); } public JDBCDriverWithPoolTest(Supplier dataSourceSupplier) { this.dataSource = dataSourceSupplier.get(); } private ExecutorService executorService = Executors.newFixedThreadPool(5); @Test void testMySQLWithConnectionPoolUsingSameContainer() throws SQLException, InterruptedException { // Populate the database with some data in multiple threads, so that multiple connections from the pool will be used for (int i = 0; i < 100; i++) { executorService.submit(() -> { try { new QueryRunner(dataSource) .insert("INSERT INTO my_counter (n) VALUES (5)", (ResultSetHandler) rs -> true); } catch (SQLException e) { e.printStackTrace(); } }); } // Complete population of the database executorService.shutdown(); executorService.awaitTermination(5, TimeUnit.MINUTES); // compare to expected results int count = new QueryRunner(dataSource) .query( "SELECT COUNT(1) FROM my_counter", rs -> { rs.next(); return rs.getInt(1); } ); assertThat(count).as("Reuse of a datasource points to the same DB container").isEqualTo(100); int sum = new QueryRunner(dataSource) .query( "SELECT SUM(n) FROM my_counter", rs -> { rs.next(); return rs.getInt(1); } ); // 100 records * 5 = 500 expected assertThat(sum).as("Reuse of a datasource points to the same DB container").isEqualTo(500); } private static DataSource getTomcatDataSourceWithDriverClassName() { PoolProperties poolProperties = new PoolProperties(); poolProperties.setUrl(URL + ";TEST=TOMCAT_WITH_CLASSNAME"); // append a dummy URL element to ensure different DB per test poolProperties.setValidationQuery("SELECT 1"); poolProperties.setMinIdle(3); poolProperties.setMaxActive(10); poolProperties.setDriverClassName(ContainerDatabaseDriver.class.getName()); return new org.apache.tomcat.jdbc.pool.DataSource(poolProperties); } private static DataSource getTomcatDataSource() { PoolProperties poolProperties = new PoolProperties(); poolProperties.setUrl(URL + ";TEST=TOMCAT"); // append a dummy URL element to ensure different DB per test poolProperties.setValidationQuery("SELECT 1"); poolProperties.setInitialSize(3); poolProperties.setMaxActive(10); return new org.apache.tomcat.jdbc.pool.DataSource(poolProperties); } private static HikariDataSource getHikariDataSourceWithDriverClassName() { HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setJdbcUrl(URL + ";TEST=HIKARI_WITH_CLASSNAME"); // append a dummy URL element to ensure different DB per test hikariConfig.setConnectionTestQuery("SELECT 1"); hikariConfig.setMinimumIdle(3); hikariConfig.setMaximumPoolSize(10); hikariConfig.setDriverClassName(ContainerDatabaseDriver.class.getName()); return new HikariDataSource(hikariConfig); } private static HikariDataSource getHikariDataSource() { HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setJdbcUrl(URL + ";TEST=HIKARI"); // append a dummy URL element to ensure different DB per test hikariConfig.setConnectionTestQuery("SELECT 1"); hikariConfig.setMinimumIdle(3); hikariConfig.setMaximumPoolSize(10); return new HikariDataSource(hikariConfig); } private static DataSource getViburDataSourceWithDriverClassName() { ViburDBCPDataSource ds = new ViburDBCPDataSource(); ds.setJdbcUrl(URL + ";TEST=VIBUR_WITH_CLASSNAME"); ds.setUsername("any"); // Recent versions of Vibur require a username, even though it will not be used ds.setPassword(""); ds.setPoolInitialSize(3); ds.setPoolMaxSize(10); ds.setTestConnectionQuery("SELECT 1"); ds.setDriverClassName(ContainerDatabaseDriver.class.getName()); ds.start(); return ds; } private static DataSource getViburDataSource() { ViburDBCPDataSource ds = new ViburDBCPDataSource(); ds.setJdbcUrl(URL + ";TEST=VIBUR"); ds.setUsername("any"); // Recent versions of Vibur require a username, even though it will not be used ds.setPassword(""); ds.setPoolInitialSize(3); ds.setPoolMaxSize(10); ds.setTestConnectionQuery("SELECT 1"); ds.start(); return ds; } @SuppressWarnings("SqlNoDataSourceInspection") public static void sampleInitFunction(Connection connection) throws SQLException { connection.createStatement().execute("CREATE TABLE bar (\n" + " foo VARCHAR(255)\n" + ");"); connection.createStatement().execute("INSERT INTO bar (foo) VALUES ('hello world');"); connection.createStatement().execute("CREATE TABLE my_counter (\n" + " n INT\n" + ");"); } } ================================================ FILE: modules/mysql/src/test/java/org/testcontainers/jdbc/mysql/MySQLDatabaseContainerDriverTest.java ================================================ package org.testcontainers.jdbc.mysql; import org.junit.jupiter.api.Test; import org.testcontainers.jdbc.ContainerDatabaseDriver; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; import static org.assertj.core.api.Assertions.assertThat; class MySQLDatabaseContainerDriverTest { @Test void shouldRespectBothUrlPropertiesAndParameterProperties() throws SQLException { ContainerDatabaseDriver driver = new ContainerDatabaseDriver(); String url = "jdbc:tc:mysql:8.0.36://hostname/databasename?padCharsWithSpace=true"; Properties properties = new Properties(); properties.setProperty("maxRows", "1"); try (Connection connection = driver.connect(url, properties)) { try (Statement statement = connection.createStatement()) { statement.execute("CREATE TABLE arbitrary_table (length_5_string CHAR(5))"); statement.execute("INSERT INTO arbitrary_table VALUES ('abc')"); statement.execute("INSERT INTO arbitrary_table VALUES ('123')"); // Check that maxRows is set try (ResultSet resultSet = statement.executeQuery("SELECT * FROM arbitrary_table")) { resultSet.next(); assertThat(resultSet.isFirst()).isTrue(); assertThat(resultSet.isLast()).isTrue(); } // Check that pad with chars is set try (ResultSet resultSet = statement.executeQuery("SELECT * FROM arbitrary_table")) { assertThat(resultSet.next()).isTrue(); assertThat(resultSet.getString(1)).isEqualTo("abc "); } } } } } ================================================ FILE: modules/mysql/src/test/java/org/testcontainers/jdbc/mysql/MySQLJDBCDriverTest.java ================================================ package org.testcontainers.jdbc.mysql; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; class MySQLJDBCDriverTest extends AbstractJDBCDriverTest { public static Iterable data() { return Arrays.asList( new Object[][] { { "jdbc:tc:mysql://hostname/databasename", EnumSet.noneOf(Options.class) }, { "jdbc:tc:mysql://hostname/databasename?user=someuser&TC_INITSCRIPT=somepath/init_mysql.sql", EnumSet.of(Options.ScriptedSchema, Options.JDBCParams), }, { "jdbc:tc:mysql:8.0.36://hostname/databasename?user=someuser&TC_INITFUNCTION=org.testcontainers.jdbc.AbstractJDBCDriverTest::sampleInitFunction", EnumSet.of(Options.ScriptedSchema, Options.JDBCParams), }, { "jdbc:tc:mysql:8.0.36://hostname/databasename?user=someuser&password=somepwd&TC_INITSCRIPT=somepath/init_mysql.sql", EnumSet.of(Options.ScriptedSchema, Options.JDBCParams), }, { "jdbc:tc:mysql:8.0.36://hostname/databasename?user=someuser&password=somepwd&TC_INITSCRIPT=file:sql/init_mysql.sql", EnumSet.of(Options.ScriptedSchema, Options.JDBCParams), }, { "jdbc:tc:mysql:8.0.36://hostname/databasename?user=someuser&password=somepwd&TC_INITFUNCTION=org.testcontainers.jdbc.AbstractJDBCDriverTest::sampleInitFunction", EnumSet.of(Options.ScriptedSchema, Options.JDBCParams), }, { "jdbc:tc:mysql:8.0.36://hostname/databasename?TC_INITSCRIPT=somepath/init_unicode_mysql.sql&useUnicode=yes&characterEncoding=utf8", EnumSet.of(Options.CharacterSet), }, { "jdbc:tc:mysql:8.0.36://hostname/databasename", EnumSet.noneOf(Options.class) }, { "jdbc:tc:mysql:8.0.36://hostname/databasename?useSSL=false", EnumSet.noneOf(Options.class) }, { "jdbc:tc:mysql:8.0.36://hostname/databasename?TC_MY_CNF=somepath/mysql_conf_override", EnumSet.of(Options.CustomIniFile), }, } ); } } ================================================ FILE: modules/mysql/src/test/java/org/testcontainers/mysql/MultiVersionMySQLTest.java ================================================ package org.testcontainers.mysql; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.testcontainers.MySQLTestImages; import org.testcontainers.db.AbstractContainerDatabaseTest; import org.testcontainers.utility.DockerImageName; import java.sql.ResultSet; import java.sql.SQLException; import static org.assertj.core.api.Assertions.assertThat; class MultiVersionMySQLTest extends AbstractContainerDatabaseTest { public static DockerImageName[] params() { return new DockerImageName[] { MySQLTestImages.MYSQL_57_IMAGE, MySQLTestImages.MYSQL_80_IMAGE, MySQLTestImages.MYSQL_INNOVATION_IMAGE, MySQLTestImages.MYSQL_93_IMAGE, }; } @ParameterizedTest @MethodSource("params") void versionCheckTest(DockerImageName dockerImageName) throws SQLException { try (MySQLContainer mysql = new MySQLContainer(dockerImageName)) { mysql.start(); final ResultSet resultSet = performQuery(mysql, "SELECT VERSION()"); final String resultSetString = resultSet.getString(1); assertThat(resultSetString) .as("The database version can be set using a container rule parameter") .isEqualTo(dockerImageName.getVersionPart()); } } } ================================================ FILE: modules/mysql/src/test/java/org/testcontainers/mysql/MySQLContainerTest.java ================================================ package org.testcontainers.mysql; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.MySQLTestImages; import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.containers.output.Slf4jLogConsumer; import org.testcontainers.db.AbstractContainerDatabaseTest; import java.io.File; import java.net.URL; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.attribute.PosixFilePermission; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assumptions.assumeThat; class MySQLContainerTest extends AbstractContainerDatabaseTest { private static final Logger logger = LoggerFactory.getLogger(MySQLContainerTest.class); @Test void testSimple() throws SQLException { try ( // container { MySQLContainer mysql = new MySQLContainer("mysql:8.0.36") // } ) { mysql.start(); ResultSet resultSet = performQuery(mysql, "SELECT 1"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); assertHasCorrectExposedAndLivenessCheckPorts(mysql); } } @Test void testSpecificVersion() throws SQLException { try ( MySQLContainer mysqlOldVersion = new MySQLContainer(MySQLTestImages.MYSQL_80_IMAGE) .withConfigurationOverride("somepath/mysql_conf_override") .withLogConsumer(new Slf4jLogConsumer(logger)) ) { mysqlOldVersion.start(); ResultSet resultSet = performQuery(mysqlOldVersion, "SELECT VERSION()"); String resultSetString = resultSet.getString(1); assertThat(resultSetString) .as("The database version can be set using a container rule parameter") .startsWith("8.0"); } } @Test void testMySQLWithCustomIniFile() throws SQLException { try ( MySQLContainer mysqlCustomConfig = new MySQLContainer(MySQLTestImages.MYSQL_80_IMAGE) .withConfigurationOverride("somepath/mysql_conf_override") ) { mysqlCustomConfig.start(); assertThatCustomIniFileWasUsed(mysqlCustomConfig); } } @Test void testCommandOverride() throws SQLException { try ( MySQLContainer mysqlCustomConfig = new MySQLContainer(MySQLTestImages.MYSQL_80_IMAGE) .withCommand("mysqld --auto_increment_increment=42") ) { mysqlCustomConfig.start(); ResultSet resultSet = performQuery(mysqlCustomConfig, "show variables like 'auto_increment_increment'"); String result = resultSet.getString("Value"); assertThat(result).as("Auto increment increment should be overridden by command line").isEqualTo("42"); } } @Test void testExplicitInitScript() throws SQLException { try ( MySQLContainer container = new MySQLContainer(MySQLTestImages.MYSQL_80_IMAGE) .withInitScript("somepath/init_mysql.sql") .withLogConsumer(new Slf4jLogConsumer(logger)) ) { container.start(); ResultSet resultSet = performQuery(container, "SELECT foo FROM bar"); String firstColumnValue = resultSet.getString(1); assertThat(firstColumnValue).as("Value from init script should equal real value").isEqualTo("hello world"); } } @Test void testEmptyPasswordWithNonRootUser() { try ( MySQLContainer container = new MySQLContainer(MySQLTestImages.MYSQL_80_IMAGE) .withDatabaseName("TEST") .withUsername("test") .withPassword("") .withEnv("MYSQL_ROOT_HOST", "%") ) { assertThatThrownBy(container::start) .isInstanceOf(ContainerLaunchException.class) .hasMessageStartingWith("Container startup failed for image mysql"); } } @Test void testEmptyPasswordWithRootUser() throws SQLException { // Add MYSQL_ROOT_HOST environment so that we can root login from anywhere for testing purposes try ( MySQLContainer mysql = new MySQLContainer(MySQLTestImages.MYSQL_80_IMAGE) .withDatabaseName("foo") .withUsername("root") .withPassword("") .withEnv("MYSQL_ROOT_HOST", "%") ) { mysql.start(); ResultSet resultSet = performQuery(mysql, "SELECT 1"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); } } @Test void testWithAdditionalUrlParamTimeZone() throws SQLException { MySQLContainer mysql = new MySQLContainer(MySQLTestImages.MYSQL_80_IMAGE) .withUrlParam("serverTimezone", "Europe/Zurich") .withEnv("TZ", "Europe/Zurich") .withLogConsumer(new Slf4jLogConsumer(logger)); mysql.start(); try (Connection connection = mysql.createConnection("")) { Statement statement = connection.createStatement(); statement.execute("SELECT NOW();"); try (ResultSet resultSet = statement.getResultSet()) { resultSet.next(); // checking that the time_zone MySQL is Europe/Zurich LocalDateTime localDateTime = resultSet.getObject(1, LocalDateTime.class); ZonedDateTime actualDateTime = localDateTime .atZone(ZoneId.of("Europe/Zurich")) .truncatedTo(ChronoUnit.MINUTES); ZonedDateTime expectedDateTime = ZonedDateTime .now(ZoneId.of("Europe/Zurich")) .truncatedTo(ChronoUnit.MINUTES); String message = String.format( "MySQL time zone is not Europe/Zurich. MySQL date:%s, current date:%s", actualDateTime, expectedDateTime ); assertThat(actualDateTime).as(message).isEqualTo(expectedDateTime); } } finally { mysql.stop(); } } @Test void testWithAdditionalUrlParamMultiQueries() throws SQLException { MySQLContainer mysql = new MySQLContainer(MySQLTestImages.MYSQL_80_IMAGE) .withUrlParam("allowMultiQueries", "true") .withLogConsumer(new Slf4jLogConsumer(logger)); mysql.start(); try (Connection connection = mysql.createConnection("")) { Statement statement = connection.createStatement(); String multiQuery = "DROP TABLE IF EXISTS bar; " + "CREATE TABLE bar (foo VARCHAR(20)); " + "INSERT INTO bar (foo) VALUES ('hello world');"; statement.execute(multiQuery); statement.execute("SELECT foo FROM bar;"); try (ResultSet resultSet = statement.getResultSet()) { resultSet.next(); String firstColumnValue = resultSet.getString(1); assertThat(firstColumnValue).as("Value from bar should equal real value").isEqualTo("hello world"); } } finally { mysql.stop(); } } @Test void testWithAdditionalUrlParamInJdbcUrl() { MySQLContainer mysql = new MySQLContainer(MySQLTestImages.MYSQL_80_IMAGE) .withUrlParam("allowMultiQueries", "true") .withUrlParam("rewriteBatchedStatements", "true") .withLogConsumer(new Slf4jLogConsumer(logger)); try { mysql.start(); String jdbcUrl = mysql.getJdbcUrl(); assertThat(jdbcUrl).contains("?"); assertThat(jdbcUrl).contains("&"); assertThat(jdbcUrl).contains("rewriteBatchedStatements=true"); assertThat(jdbcUrl).contains("allowMultiQueries=true"); } finally { mysql.stop(); } } @Test void testWithOnlyUserReadableCustomIniFile() throws Exception { assumeThat(FileSystems.getDefault().supportedFileAttributeViews().contains("posix")).isTrue(); try ( MySQLContainer mysql = new MySQLContainer(MySQLTestImages.MYSQL_80_IMAGE) .withConfigurationOverride("somepath/mysql_conf_override") .withLogConsumer(new Slf4jLogConsumer(logger)) ) { URL resource = this.getClass().getClassLoader().getResource("somepath/mysql_conf_override"); File file = new File(resource.toURI()); assertThat(file.isDirectory()).isTrue(); Set permissions = new HashSet<>( Arrays.asList( PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE ) ); Files.setPosixFilePermissions(file.toPath(), permissions); mysql.start(); assertThatCustomIniFileWasUsed(mysql); } } @Test void testCustom() throws SQLException { // Add MYSQL_ROOT_HOST environment so that we can root login from anywhere for testing purposes try ( MySQLContainer mysql = new MySQLContainer(MySQLTestImages.MYSQL_80_IMAGE) .withDatabaseName("foo") .withUsername("bar") .withPassword("baz") .withEnv("MYSQL_ROOT_HOST", "%") ) { mysql.start(); ResultSet resultSet = performQuery(mysql, "SELECT 1"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); } } private void assertHasCorrectExposedAndLivenessCheckPorts(MySQLContainer mysql) { assertThat(mysql.getExposedPorts()).containsExactly(MySQLContainer.MYSQL_PORT); assertThat(mysql.getLivenessCheckPortNumbers()).containsExactly(mysql.getMappedPort(MySQLContainer.MYSQL_PORT)); } private void assertThatCustomIniFileWasUsed(MySQLContainer mysql) throws SQLException { try (ResultSet resultSet = performQuery(mysql, "SELECT @@GLOBAL.innodb_max_undo_log_size")) { long result = resultSet.getLong(1); assertThat(result) .as("The InnoDB max undo log size has been set by the ini file content") .isEqualTo(20000000); } } } ================================================ FILE: modules/mysql/src/test/java/org/testcontainers/mysql/MySQLR2DBCDatabaseContainerTest.java ================================================ package org.testcontainers.mysql; import io.r2dbc.spi.ConnectionFactoryOptions; import org.testcontainers.MySQLTestImages; import org.testcontainers.r2dbc.AbstractR2DBCDatabaseContainerTest; public class MySQLR2DBCDatabaseContainerTest extends AbstractR2DBCDatabaseContainerTest { @Override protected ConnectionFactoryOptions getOptions(MySQLContainer container) { return MySQLR2DBCDatabaseContainer.getOptions(container); } @Override protected String createR2DBCUrl() { return "r2dbc:tc:mysql:///db?TC_IMAGE_TAG=" + MySQLTestImages.MYSQL_80_IMAGE.getVersionPart(); } @Override protected MySQLContainer createContainer() { return new MySQLContainer(MySQLTestImages.MYSQL_80_IMAGE); } } ================================================ FILE: modules/mysql/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/mysql/src/test/resources/somepath/init_mysql.sql ================================================ CREATE TABLE bar ( foo VARCHAR(255) ); DROP PROCEDURE IF EXISTS -- ; count_foo; SELECT "a /* string literal containing comment characters like -- here"; SELECT "a 'quoting' \"scenario ` involving BEGIN keyword\" here"; SELECT * from `bar`; -- What about a line comment containing imbalanced string delimiters? " CREATE PROCEDURE count_foo() BEGIN BEGIN SELECT * FROM bar; SELECT 1 FROM dual; END; BEGIN select * from bar; END; -- we can do comments /* including block comments */ /* what if BEGIN appears inside a comment? */ select "or what if BEGIN appears inside a literal?"; END /*; */; /* or a block comment containing imbalanced string delimiters? ' " */ INSERT INTO bar (foo) /* ; */ VALUES ('hello world'); ================================================ FILE: modules/mysql/src/test/resources/somepath/init_unicode_mysql.sql ================================================ CREATE TABLE bar ( foo varchar(255) character set utf8 ); INSERT INTO bar (foo) VALUES ('привет мир'); ================================================ FILE: modules/mysql/src/test/resources/somepath/mysql_conf_override/my.cnf ================================================ [mysqld] user = mysql datadir = /var/lib/mysql port = 3306 #socket = /tmp/mysql.sock skip-external-locking key_buffer_size = 16K max_allowed_packet = 1M table_open_cache = 4 sort_buffer_size = 64K read_buffer_size = 256K read_rnd_buffer_size = 256K net_buffer_length = 2K host_cache_size = 0 skip-name-resolve # This configuration is custom to test whether config override works innodb_max_undo_log_size = 20000000 # Don't listen on a TCP/IP port at all. This can be a security enhancement, # if all processes that need to connect to mysqld run on the same host. # All interaction with mysqld must be made via Unix sockets or named pipes. # Note that using this option without enabling named pipes on Windows # (using the "enable-named-pipe" option) will render mysqld useless! # #skip-networking #server-id = 1 # Uncomment the following if you want to log updates #log-bin=mysql-bin # binary logging format - mixed recommended #binlog_format=mixed # Causes updates to non-transactional engines using statement format to be # written directly to binary log. Before using this option make sure that # there are no dependencies between transactional and non-transactional # tables such as in the statement INSERT INTO t_myisam SELECT * FROM # t_innodb; otherwise, slaves may diverge from the master. #binlog_direct_non_transactional_updates=TRUE # Uncomment the following if you are using InnoDB tables innodb_data_file_path = ibdata1:10M:autoextend # You can set .._buffer_pool_size up to 50 - 80 % # of RAM but beware of setting memory usage too high innodb_buffer_pool_size = 16M #innodb_additional_mem_pool_size = 2M # Set .._log_file_size to 25 % of buffer pool size innodb_log_buffer_size = 8M innodb_flush_log_at_trx_commit = 1 innodb_lock_wait_timeout = 50 ================================================ FILE: modules/neo4j/.gitignore ================================================ container-license-acceptance.txt ================================================ FILE: modules/neo4j/build.gradle ================================================ description = "Testcontainers :: Neo4j" def generatedResourcesDir = new File(project.buildDir, "generated-resources") def customNeo4jPluginDestinationDir = new File(generatedResourcesDir, "custom-plugins") sourceSets { customNeo4jPlugin { java { srcDir 'src/custom-neo4j-plugin' } } test { resources { srcDir generatedResourcesDir } } } task customNeo4jPluginJar(type: Jar) { from sourceSets.customNeo4jPlugin.output archiveFileName = "hello-world.jar" destinationDirectory = customNeo4jPluginDestinationDir inputs.files(sourceSets.customNeo4jPlugin.java.srcDirs) outputs.cacheIf { true } outputs.dir(customNeo4jPluginDestinationDir) } processTestResources.dependsOn customNeo4jPluginJar dependencies { customNeo4jPluginCompileOnly "org.neo4j:neo4j:3.5.35" api project(":testcontainers") testImplementation 'org.neo4j.driver:neo4j-java-driver:4.4.21' } ================================================ FILE: modules/neo4j/src/custom-neo4j-plugin/java/ac/simons/neo4j/demos/plugins/HelloWorld.java ================================================ package ac.simons.neo4j.demos.plugins; import org.neo4j.procedure.Description; import org.neo4j.procedure.Name; import org.neo4j.procedure.UserFunction; public class HelloWorld { @UserFunction("ac.simons.helloWorld") @Description("Simple Hello World") public String helloWorld(@Name("name") String name) { return "Hello, " + name; } } ================================================ FILE: modules/neo4j/src/main/java/org/testcontainers/containers/Neo4jContainer.java ================================================ package org.testcontainers.containers; import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; import org.testcontainers.containers.wait.strategy.WaitAllStrategy; import org.testcontainers.containers.wait.strategy.WaitStrategy; import org.testcontainers.utility.ComparableVersion; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.LicenseAcceptance; import org.testcontainers.utility.MountableFile; import java.net.HttpURLConnection; import java.time.Duration; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; /** * Testcontainers implementation for Neo4j. *

* Supported image: {@code neo4j} *

* Exposed ports: *

    *
  • Bolt: 7687
  • *
  • HTTP: 7474
  • *
  • HTTPS: 7473
  • *
* * @deprecated use {@link org.testcontainers.neo4j.Neo4jContainer} instead. */ @Deprecated public class Neo4jContainer> extends GenericContainer { /** * The image defaults to the official Neo4j image: Neo4j. */ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("neo4j"); /** * The default tag (version) to use. */ private static final String DEFAULT_TAG = "4.4"; private static final String ENTERPRISE_TAG = DEFAULT_TAG + "-enterprise"; /** * Default port for the binary Bolt protocol. */ private static final int DEFAULT_BOLT_PORT = 7687; /** * The port of the transactional HTTPS endpoint: Neo4j REST API. */ private static final int DEFAULT_HTTPS_PORT = 7473; /** * The port of the transactional HTTP endpoint: Neo4j REST API. */ private static final int DEFAULT_HTTP_PORT = 7474; /** * The official image requires a change of password by default from "neo4j" to something else. This defaults to "password". */ private static final String DEFAULT_ADMIN_PASSWORD = "password"; private static final String AUTH_FORMAT = "neo4j/%s"; private final boolean standardImage; private String adminPassword = DEFAULT_ADMIN_PASSWORD; private final Set labsPlugins = new HashSet<>(); /** * Default wait strategies */ public static final WaitStrategy WAIT_FOR_BOLT = new LogMessageWaitStrategy() .withRegEx(String.format(".*Bolt enabled on .*:%d\\.\n", DEFAULT_BOLT_PORT)); private static final WaitStrategy WAIT_FOR_HTTP = new HttpWaitStrategy() .forPort(DEFAULT_HTTP_PORT) .forStatusCodeMatching(response -> response == HttpURLConnection.HTTP_OK); /** * Creates a Neo4jContainer using the official Neo4j docker image. * @deprecated use {@link #Neo4jContainer(DockerImageName)} instead */ @Deprecated public Neo4jContainer() { this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } /** * Creates a Neo4jContainer using a specific docker image. * * @param dockerImageName The docker image to use. */ public Neo4jContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } /** * Creates a Neo4jContainer using a specific docker image. * * @param dockerImageName The docker image to use. */ public Neo4jContainer(final DockerImageName dockerImageName) { super(dockerImageName); this.standardImage = dockerImageName.getUnversionedPart().equals(DEFAULT_IMAGE_NAME.getUnversionedPart()); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); this.waitStrategy = new WaitAllStrategy() .withStrategy(WAIT_FOR_BOLT) .withStrategy(WAIT_FOR_HTTP) .withStartupTimeout(Duration.ofMinutes(2)); addExposedPorts(DEFAULT_BOLT_PORT, DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT); } @Override public Set getLivenessCheckPortNumbers() { return Stream .of(DEFAULT_BOLT_PORT, DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT) .map(this::getMappedPort) .collect(Collectors.toSet()); } @Override protected void configure() { configureAuth(); configureLabsPlugins(); configureWaitStrategy(); } /** * Configured via {@link Neo4jContainer#withAdminPassword(String)} or {@link Neo4jContainer#withoutAuthentication()} * It is only possible to set the correct auth in the configuration call. * Also, the custom methods overrule the set env parameter. */ private void configureAuth() { String neo4jAuthEnvKey = "NEO4J_AUTH"; if (!getEnvMap().containsKey(neo4jAuthEnvKey) || !DEFAULT_ADMIN_PASSWORD.equals(this.adminPassword)) { boolean emptyAdminPassword = this.adminPassword == null || this.adminPassword.isEmpty(); String neo4jAuth = emptyAdminPassword ? "none" : String.format(AUTH_FORMAT, this.adminPassword); addEnv(neo4jAuthEnvKey, neo4jAuth); } } /** * Configured via {@link Neo4jContainer#withLabsPlugins}. * Configuration can only happen in the configuration call because there is no default. */ private void configureLabsPlugins() { String neo4jLabsPluginsEnvKey = "NEO4JLABS_PLUGINS"; if (!getEnv().contains(neo4jLabsPluginsEnvKey) && !this.labsPlugins.isEmpty()) { String enabledPlugins = this.labsPlugins.stream().map(pluginName -> "\"" + pluginName + "\"").collect(Collectors.joining(",")); addEnv(neo4jLabsPluginsEnvKey, "[" + enabledPlugins + "]"); } } /** * Update the default Neo4jContainer wait strategy based on the exposed ports. * Still possible to override the startup timeout before starting the container via {@link WaitStrategy#withStartupTimeout(Duration)}. */ private void configureWaitStrategy() { List exposedPorts = getExposedPorts(); boolean boltExposed = exposedPorts.contains(DEFAULT_BOLT_PORT); boolean httpExposed = exposedPorts.contains(DEFAULT_HTTP_PORT); boolean onlyBoltExposed = boltExposed && !httpExposed; boolean onlyHttpExposed = !boltExposed && httpExposed; if (onlyBoltExposed) { this.waitStrategy = new WaitAllStrategy().withStrategy(WAIT_FOR_BOLT).withStartupTimeout(Duration.ofMinutes(2)); } else if (onlyHttpExposed) { this.waitStrategy = new WaitAllStrategy().withStrategy(WAIT_FOR_HTTP).withStartupTimeout(Duration.ofMinutes(2)); } } /** * @return Bolt URL for use with Neo4j's Java-Driver. */ public String getBoltUrl() { return String.format("bolt://" + getHost() + ":" + getMappedPort(DEFAULT_BOLT_PORT)); } /** * @return URL of the transactional HTTP endpoint. */ public String getHttpUrl() { return String.format("http://" + getHost() + ":" + getMappedPort(DEFAULT_HTTP_PORT)); } /** * @return URL of the transactional HTTPS endpoint. */ public String getHttpsUrl() { return String.format("https://" + getHost() + ":" + getMappedPort(DEFAULT_HTTPS_PORT)); } /** * Configures the container to use the enterprise edition of the default docker image. *

* Please have a look at the Neo4j Licensing page. While the Neo4j * Community Edition can be used for free in your projects under the GPL v3 license, Neo4j Enterprise edition * needs either a commercial, education or evaluation license. * * @return This container. */ public S withEnterpriseEdition() { if (!standardImage) { throw new IllegalStateException( String.format("Cannot use enterprise version with alternative image %s.", getDockerImageName()) ); } setDockerImageName(DEFAULT_IMAGE_NAME.withTag(ENTERPRISE_TAG).asCanonicalNameString()); LicenseAcceptance.assertLicenseAccepted(getDockerImageName()); addEnv("NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes"); return self(); } /** * Sets the admin password for the default account (which is
neo4j
). A null value or an empty string * disables authentication. * * @param adminPassword The admin password for the default database account. * @return This container. */ public S withAdminPassword(final String adminPassword) { if (adminPassword != null && adminPassword.length() < 8) { logger().warn("Your provided admin password is too short and will not work with Neo4j 5.3+."); } this.adminPassword = adminPassword; return self(); } /** * Disables authentication. * * @return This container. */ public S withoutAuthentication() { return withAdminPassword(null); } /** * Copies an existing {@code graph.db} folder into the container. This can either be a classpath resource or a * host resource. Please have a look at the factory methods in {@link MountableFile}. *
* If you want to map your database into the container instead of copying them, please use {@code #withClasspathResourceMapping}, * but this will only work when your test does not run in a container itself. *
* Note: This method only works with Neo4j 3.5. *
* Mapping would work like this: *
     *      @Container
     *      private static final Neo4jContainer databaseServer = new Neo4jContainer<>()
     *          .withClasspathResourceMapping("/test-graph.db", "/data/databases/graph.db", BindMode.READ_WRITE);
     * 
* * @param graphDb The graph.db folder to copy into the container * @throws IllegalArgumentException If the database version is not 3.5. * @return This container. */ public S withDatabase(MountableFile graphDb) { if (!isNeo4jDatabaseVersionSupportingDbCopy()) { throw new IllegalArgumentException( "Copying database folder is not supported for Neo4j instances with version 4.0 or higher." ); } return withCopyFileToContainer(graphDb, "/data/databases/graph.db"); } /** * Adds plugins to the given directory to the container. If {@code plugins} denotes a directory, than all of that * directory is mapped to Neo4j's plugins. Otherwise, single resources are copied over. *
* If you want to map your plugins into the container instead of copying them, please use {@code #withClasspathResourceMapping}, * but this will only work when your test does not run in a container itself. * * @param plugins * @return This container. */ public S withPlugins(MountableFile plugins) { return withCopyFileToContainer(plugins, "/var/lib/neo4j/plugins/"); } /** * Adds Neo4j configuration properties to the container. The properties can be added as in the official Neo4j * configuration, the method automatically translate them into the format required by the Neo4j container. * * @param key The key to configure, i.e. {@code dbms.security.procedures.unrestricted} * @param value The value to set * @return This container. */ public S withNeo4jConfig(String key, String value) { addEnv(formatConfigurationKey(key), value); return self(); } /** * @return The admin password for the neo4j account or literal null if auth is disabled. */ public String getAdminPassword() { return adminPassword; } /** * Registers one or more Neo4j plugins for server startup. * The plugins are listed here * * * @param plugins The Neo4j plugins that should get started with the server. * @return This container. */ public S withPlugins(String... plugins) { this.labsPlugins.addAll(Arrays.asList(plugins)); return self(); } private static String formatConfigurationKey(String plainConfigKey) { final String prefix = "NEO4J_"; return String.format("%s%s", prefix, plainConfigKey.replaceAll("_", "__").replaceAll("\\.", "_")); } private boolean isNeo4jDatabaseVersionSupportingDbCopy() { String usedImageVersion = DockerImageName.parse(getDockerImageName()).getVersionPart(); ComparableVersion usedComparableVersion = new ComparableVersion(usedImageVersion); boolean versionSupportingDbCopy = usedComparableVersion.isLessThan("4.0") && usedComparableVersion.isGreaterThanOrEqualTo("2"); if (versionSupportingDbCopy) { return true; } if (!usedComparableVersion.isSemanticVersion()) { logger() .warn( "Version {} is not a semantic version. The function \"withDatabase\" will fail.", usedImageVersion ); logger().warn("Copying databases is only supported for Neo4j versions 3.5.x"); } return false; } public S withRandomPassword() { return withAdminPassword(UUID.randomUUID().toString()); } } ================================================ FILE: modules/neo4j/src/main/java/org/testcontainers/neo4j/Neo4jContainer.java ================================================ package org.testcontainers.neo4j; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; import org.testcontainers.containers.wait.strategy.WaitAllStrategy; import org.testcontainers.containers.wait.strategy.WaitStrategy; import org.testcontainers.utility.ComparableVersion; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.net.HttpURLConnection; import java.time.Duration; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; /** * Testcontainers implementation for Neo4j. *

* Supported image: {@code neo4j} *

* Exposed ports: *

    *
  • Bolt: 7687
  • *
  • HTTP: 7474
  • *
  • HTTPS: 7473
  • *
*/ public class Neo4jContainer extends GenericContainer { /** * The image defaults to the official Neo4j image: Neo4j. */ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("neo4j"); /** * Default port for the binary Bolt protocol. */ private static final int DEFAULT_BOLT_PORT = 7687; /** * The port of the transactional HTTPS endpoint: Neo4j REST API. */ private static final int DEFAULT_HTTPS_PORT = 7473; /** * The port of the transactional HTTP endpoint: Neo4j REST API. */ private static final int DEFAULT_HTTP_PORT = 7474; /** * The official image requires a change of password by default from "neo4j" to something else. This defaults to "password". */ private static final String DEFAULT_ADMIN_PASSWORD = "password"; private static final String AUTH_FORMAT = "neo4j/%s"; private String adminPassword = DEFAULT_ADMIN_PASSWORD; private final Set labsPlugins = new HashSet<>(); /** * Default wait strategies */ public static final WaitStrategy WAIT_FOR_BOLT = new LogMessageWaitStrategy() .withRegEx(String.format(".*Bolt enabled on .*:%d\\.\n", DEFAULT_BOLT_PORT)); private static final WaitStrategy WAIT_FOR_HTTP = new HttpWaitStrategy() .forPort(DEFAULT_HTTP_PORT) .forStatusCodeMatching(response -> response == HttpURLConnection.HTTP_OK); /** * Creates a Neo4jContainer using a specific docker image. * * @param dockerImageName The docker image to use. */ public Neo4jContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } /** * Creates a Neo4jContainer using a specific docker image. * * @param dockerImageName The docker image to use. */ public Neo4jContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); waitingFor( new WaitAllStrategy() .withStrategy(WAIT_FOR_BOLT) .withStrategy(WAIT_FOR_HTTP) .withStartupTimeout(Duration.ofMinutes(2)) ); addExposedPorts(DEFAULT_BOLT_PORT, DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT); } @Override public Set getLivenessCheckPortNumbers() { return Stream .of(DEFAULT_BOLT_PORT, DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT) .map(this::getMappedPort) .collect(Collectors.toSet()); } @Override protected void configure() { configureAuth(); configureLabsPlugins(); configureWaitStrategy(); } /** * Configured via {@link Neo4jContainer#withAdminPassword(String)} or {@link Neo4jContainer#withoutAuthentication()} * It is only possible to set the correct auth in the configuration call. * Also, the custom methods overrule the set env parameter. */ private void configureAuth() { String neo4jAuthEnvKey = "NEO4J_AUTH"; if (!getEnvMap().containsKey(neo4jAuthEnvKey) || !DEFAULT_ADMIN_PASSWORD.equals(this.adminPassword)) { boolean emptyAdminPassword = this.adminPassword == null || this.adminPassword.isEmpty(); String neo4jAuth = emptyAdminPassword ? "none" : String.format(AUTH_FORMAT, this.adminPassword); addEnv(neo4jAuthEnvKey, neo4jAuth); } } /** * Configured via {@link Neo4jContainer#withLabsPlugins}. * Configuration can only happen in the configuration call because there is no default. */ private void configureLabsPlugins() { String neo4jLabsPluginsEnvKey = "NEO4JLABS_PLUGINS"; if (!getEnv().contains(neo4jLabsPluginsEnvKey) && !this.labsPlugins.isEmpty()) { String enabledPlugins = this.labsPlugins.stream().map(pluginName -> "\"" + pluginName + "\"").collect(Collectors.joining(",")); addEnv(neo4jLabsPluginsEnvKey, "[" + enabledPlugins + "]"); } } /** * Update the default Neo4jContainer wait strategy based on the exposed ports. * Still possible to override the startup timeout before starting the container via {@link WaitStrategy#withStartupTimeout(Duration)}. */ private void configureWaitStrategy() { List exposedPorts = getExposedPorts(); boolean boltExposed = exposedPorts.contains(DEFAULT_BOLT_PORT); boolean httpExposed = exposedPorts.contains(DEFAULT_HTTP_PORT); boolean onlyBoltExposed = boltExposed && !httpExposed; boolean onlyHttpExposed = !boltExposed && httpExposed; if (onlyBoltExposed) { waitingFor(new WaitAllStrategy().withStrategy(WAIT_FOR_BOLT).withStartupTimeout(Duration.ofMinutes(2))); } else if (onlyHttpExposed) { waitingFor(new WaitAllStrategy().withStrategy(WAIT_FOR_HTTP).withStartupTimeout(Duration.ofMinutes(2))); } } /** * @return Bolt URL for use with Neo4j's Java-Driver. */ public String getBoltUrl() { return String.format("bolt://" + getHost() + ":" + getMappedPort(DEFAULT_BOLT_PORT)); } /** * @return URL of the transactional HTTP endpoint. */ public String getHttpUrl() { return String.format("http://" + getHost() + ":" + getMappedPort(DEFAULT_HTTP_PORT)); } /** * @return URL of the transactional HTTPS endpoint. */ public String getHttpsUrl() { return String.format("https://" + getHost() + ":" + getMappedPort(DEFAULT_HTTPS_PORT)); } /** * Accepts the license agreement of the container. * * @return this */ public Neo4jContainer acceptLicense() { addEnv("NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes"); return self(); } /** * Sets the admin password for the default account (which is
neo4j
). A null value or an empty string * disables authentication. * * @param adminPassword The admin password for the default database account. * @return This container. */ public Neo4jContainer withAdminPassword(final String adminPassword) { if (adminPassword != null && adminPassword.length() < 8) { logger().warn("Your provided admin password is too short and will not work with Neo4j 5.3+."); } this.adminPassword = adminPassword; return self(); } /** * Disables authentication. * * @return This container. */ public Neo4jContainer withoutAuthentication() { return withAdminPassword(null); } /** * Copies an existing {@code graph.db} folder into the container. This can either be a classpath resource or a * host resource. Please have a look at the factory methods in {@link MountableFile}. *
* If you want to map your database into the container instead of copying them, please use {@code #withClasspathResourceMapping}, * but this will only work when your test does not run in a container itself. *
* Note: This method only works with Neo4j 3.5. *
* Mapping would work like this: *
     *      @Container
     *      private static final Neo4jContainer databaseServer = new Neo4jContainer<>()
     *          .withClasspathResourceMapping("/test-graph.db", "/data/databases/graph.db", BindMode.READ_WRITE);
     * 
* * @param graphDb The graph.db folder to copy into the container * @throws IllegalArgumentException If the database version is not 3.5. * @return This container. */ public Neo4jContainer withDatabase(MountableFile graphDb) { if (!isNeo4jDatabaseVersionSupportingDbCopy()) { throw new IllegalArgumentException( "Copying database folder is not supported for Neo4j instances with version 4.0 or higher." ); } return withCopyFileToContainer(graphDb, "/data/databases/graph.db"); } /** * Adds plugins to the given directory to the container. If {@code plugins} denotes a directory, than all of that * directory is mapped to Neo4j's plugins. Otherwise, single resources are copied over. *
* If you want to map your plugins into the container instead of copying them, please use {@code #withClasspathResourceMapping}, * but this will only work when your test does not run in a container itself. * * @param plugins * @return This container. */ public Neo4jContainer withPlugins(MountableFile plugins) { return withCopyFileToContainer(plugins, "/var/lib/neo4j/plugins/"); } /** * Adds Neo4j configuration properties to the container. The properties can be added as in the official Neo4j * configuration, the method automatically translate them into the format required by the Neo4j container. * * @param key The key to configure, i.e. {@code dbms.security.procedures.unrestricted} * @param value The value to set * @return This container. */ public Neo4jContainer withNeo4jConfig(String key, String value) { addEnv(formatConfigurationKey(key), value); return self(); } /** * @return The admin password for the neo4j account or literal null if auth is disabled. */ public String getAdminPassword() { return adminPassword; } /** * Registers one or more Neo4j plugins for server startup. * The plugins are listed here * * * @param plugins The Neo4j plugins that should get started with the server. * @return This container. */ public Neo4jContainer withPlugins(String... plugins) { this.labsPlugins.addAll(Arrays.asList(plugins)); return self(); } private static String formatConfigurationKey(String plainConfigKey) { final String prefix = "NEO4J_"; return String.format("%s%s", prefix, plainConfigKey.replaceAll("_", "__").replaceAll("\\.", "_")); } private boolean isNeo4jDatabaseVersionSupportingDbCopy() { String usedImageVersion = DockerImageName.parse(getDockerImageName()).getVersionPart(); ComparableVersion usedComparableVersion = new ComparableVersion(usedImageVersion); boolean versionSupportingDbCopy = usedComparableVersion.isLessThan("4.0") && usedComparableVersion.isGreaterThanOrEqualTo("2"); if (versionSupportingDbCopy) { return true; } if (!usedComparableVersion.isSemanticVersion()) { logger() .warn( "Version {} is not a semantic version. The function \"withDatabase\" will fail.", usedImageVersion ); logger().warn("Copying databases is only supported for Neo4j versions 3.5.x"); } return false; } public Neo4jContainer withRandomPassword() { return withAdminPassword(UUID.randomUUID().toString()); } } ================================================ FILE: modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java ================================================ package org.testcontainers.neo4j; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.AppenderBase; import org.junit.jupiter.api.Test; import org.neo4j.driver.AuthToken; import org.neo4j.driver.AuthTokens; import org.neo4j.driver.Driver; import org.neo4j.driver.GraphDatabase; import org.neo4j.driver.Record; import org.neo4j.driver.Result; import org.neo4j.driver.Session; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy; import org.testcontainers.utility.DockerLoggerFactory; import org.testcontainers.utility.MountableFile; import java.util.Collections; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assumptions.assumeThat; class Neo4jContainerTest { // See org.testcontainers.utility.LicenseAcceptance#ACCEPTANCE_FILE_NAME private static final String ACCEPTANCE_FILE_LOCATION = "/container-license-acceptance.txt"; @Test void shouldDisableAuthentication() { try ( // spotless:off // withoutAuthentication { Neo4jContainer neo4jContainer = new Neo4jContainer("neo4j:4.4") .withoutAuthentication() // } // spotless:on ) { neo4jContainer.start(); try (Driver driver = getDriver(neo4jContainer); Session session = driver.session()) { long one = session.run("RETURN 1", Collections.emptyMap()).next().get(0).asLong(); assertThat(one).isEqualTo(1L); } } } @Test void shouldCopyDatabase() { // no aarch64 image available for Neo4j 3.5 assumeThat(DockerClientFactory.instance().getInfo().getArchitecture()).isNotEqualTo("aarch64"); try ( // copyDatabase { Neo4jContainer neo4jContainer = new Neo4jContainer("neo4j:3.5.30") .withDatabase(MountableFile.forClasspathResource("/test-graph.db")) // } ) { neo4jContainer.start(); try (Driver driver = getDriver(neo4jContainer); Session session = driver.session()) { Result result = session.run("MATCH (t:Thing) RETURN t"); assertThat(result.list().stream().map(r -> r.get("t").get("name").asString())) .containsExactlyInAnyOrder("Thing", "Thing 2", "Thing 3", "A box"); } } } @Test void shouldFailOnCopyDatabaseForDefaultNeo4j4Image() { assertThatIllegalArgumentException() .isThrownBy(() -> { new Neo4jContainer("neo4j:4.4.1").withDatabase(MountableFile.forClasspathResource("/test-graph.db")); }) .withMessage("Copying database folder is not supported for Neo4j instances with version 4.0 or higher."); } @Test void shouldFailOnCopyDatabaseForCustomNeo4j4Image() { assertThatIllegalArgumentException() .isThrownBy(() -> { new Neo4jContainer("neo4j:4.4.1").withDatabase(MountableFile.forClasspathResource("/test-graph.db")); }) .withMessage("Copying database folder is not supported for Neo4j instances with version 4.0 or higher."); } @Test void shouldFailOnCopyDatabaseForCustomNonSemverNeo4j4Image() { assertThatIllegalArgumentException() .isThrownBy(() -> { new Neo4jContainer("neo4j:latest").withDatabase(MountableFile.forClasspathResource("/test-graph.db")); }) .withMessage("Copying database folder is not supported for Neo4j instances with version 4.0 or higher."); } @Test void shouldCopyPlugins() { try ( // registerPluginsPath { Neo4jContainer neo4jContainer = new Neo4jContainer("neo4j:4.4") .withPlugins(MountableFile.forClasspathResource("/custom-plugins")) // } ) { neo4jContainer.start(); try (Driver driver = getDriver(neo4jContainer); Session session = driver.session()) { assertThatCustomPluginWasCopied(session); } } } @Test void shouldCopyPlugin() { try ( // registerPluginsJar { Neo4jContainer neo4jContainer = new Neo4jContainer("neo4j:4.4") .withPlugins(MountableFile.forClasspathResource("/custom-plugins/hello-world.jar")) // } ) { neo4jContainer.start(); try (Driver driver = getDriver(neo4jContainer); Session session = driver.session()) { assertThatCustomPluginWasCopied(session); } } } private static void assertThatCustomPluginWasCopied(Session session) { Result result = session.run("RETURN ac.simons.helloWorld('Testcontainers') AS greeting"); Record singleRecord = result.single(); assertThat(singleRecord).isNotNull(); assertThat(singleRecord.get("greeting").asString()).isEqualTo("Hello, Testcontainers"); } @Test void shouldRunEnterprise() { assumeThat(Neo4jContainerTest.class.getResource(ACCEPTANCE_FILE_LOCATION)).isNotNull(); try ( // enterpriseEdition { Neo4jContainer neo4jContainer = new Neo4jContainer("neo4j:4.4-enterprise") .acceptLicense() // } .withAdminPassword("Picard123") ) { neo4jContainer.start(); try (Driver driver = getDriver(neo4jContainer); Session session = driver.session()) { String edition = session .run("CALL dbms.components() YIELD edition RETURN edition", Collections.emptyMap()) .next() .get(0) .asString(); assertThat(edition).isEqualTo("enterprise"); } } } @Test void shouldAddConfigToEnvironment() { // neo4jConfiguration { Neo4jContainer neo4jContainer = new Neo4jContainer("neo4j:4.4") .withNeo4jConfig("dbms.security.procedures.unrestricted", "apoc.*,algo.*") .withNeo4jConfig("dbms.tx_log.rotation.size", "42M"); // } assertThat(neo4jContainer.getEnvMap()) .containsEntry("NEO4J_dbms_security_procedures_unrestricted", "apoc.*,algo.*"); assertThat(neo4jContainer.getEnvMap()).containsEntry("NEO4J_dbms_tx__log_rotation_size", "42M"); } @Test void shouldRespectEnvironmentAuth() { Neo4jContainer neo4jContainer = new Neo4jContainer("neo4j:4.4").withEnv("NEO4J_AUTH", "neo4j/secret"); neo4jContainer.configure(); assertThat(neo4jContainer.getEnvMap()).containsEntry("NEO4J_AUTH", "neo4j/secret"); } @Test void shouldSetCustomPasswordCorrectly() { // withAdminPassword { Neo4jContainer neo4jContainer = new Neo4jContainer("neo4j:4.4").withAdminPassword("verySecret"); // } neo4jContainer.configure(); assertThat(neo4jContainer.getEnvMap()).containsEntry("NEO4J_AUTH", "neo4j/verySecret"); } @Test void containerAdminPasswordOverrulesEnvironmentAuth() { Neo4jContainer neo4jContainer = new Neo4jContainer("neo4j:4.4") .withEnv("NEO4J_AUTH", "neo4j/secret") .withAdminPassword("anotherSecret"); neo4jContainer.configure(); assertThat(neo4jContainer.getEnvMap()).containsEntry("NEO4J_AUTH", "neo4j/anotherSecret"); } @Test void containerWithoutAuthenticationOverrulesEnvironmentAuth() { Neo4jContainer neo4jContainer = new Neo4jContainer("neo4j:4.4") .withEnv("NEO4J_AUTH", "neo4j/secret") .withoutAuthentication(); neo4jContainer.configure(); assertThat(neo4jContainer.getEnvMap()).containsEntry("NEO4J_AUTH", "none"); } @Test void shouldRespectAlreadyDefinedPortMappingsBolt() { Neo4jContainer neo4jContainer = new Neo4jContainer("neo4j:4.4").withExposedPorts(7687); neo4jContainer.configure(); assertThat(neo4jContainer.getExposedPorts()).containsExactly(7687); } @Test void shouldRespectAlreadyDefinedPortMappingsHttp() { Neo4jContainer neo4jContainer = new Neo4jContainer("neo4j:4.4").withExposedPorts(7474); neo4jContainer.configure(); assertThat(neo4jContainer.getExposedPorts()).containsExactly(7474); } @Test void shouldRespectAlreadyDefinedPortMappingsWithoutHttps() { Neo4jContainer neo4jContainer = new Neo4jContainer("neo4j:4.4").withExposedPorts(7687, 7474); neo4jContainer.configure(); assertThat(neo4jContainer.getExposedPorts()).containsExactlyInAnyOrder(7474, 7687); } @Test void shouldDefaultExportBoltHttpAndHttps() { Neo4jContainer neo4jContainer = new Neo4jContainer("neo4j:4.4"); neo4jContainer.configure(); assertThat(neo4jContainer.getExposedPorts()).containsExactlyInAnyOrder(7473, 7474, 7687); } @Test void shouldRespectCustomWaitStrategy() { Neo4jContainer neo4jContainer = new Neo4jContainer("neo4j:4.4").waitingFor(new CustomDummyWaitStrategy()); neo4jContainer.configure(); assertThat(neo4jContainer).extracting("waitStrategy").isInstanceOf(CustomDummyWaitStrategy.class); } @Test void shouldConfigureSinglePluginByName() { try (Neo4jContainer neo4jContainer = new Neo4jContainer("neo4j:4.4").withPlugins("apoc")) { // needs to get called explicitly for setup neo4jContainer.configure(); assertThat(neo4jContainer.getEnvMap()).containsEntry("NEO4JLABS_PLUGINS", "[\"apoc\"]"); } } @Test void shouldConfigureMultiplePluginsByName() { try ( // configureLabsPlugins { Neo4jContainer neo4jContainer = new Neo4jContainer("neo4j:4.4") // .withPlugins("apoc", "bloom"); // } ) { // needs to get called explicitly for setup neo4jContainer.configure(); assertThat(neo4jContainer.getEnvMap().get("NEO4JLABS_PLUGINS")) .containsAnyOf("[\"apoc\",\"bloom\"]", "[\"bloom\",\"apoc\"]"); } } @Test void shouldCreateRandomUuidBasedPasswords() { try ( // withRandomPassword { Neo4jContainer neo4jContainer = new Neo4jContainer("neo4j:4.4").withRandomPassword(); // } ) { // It will throw an exception if it's not UUID parsable. assertThatNoException().isThrownBy(neo4jContainer::configure); // This basically is always true at if the random password is UUID-like. assertThat(neo4jContainer.getAdminPassword()) .satisfies(password -> assertThat(UUID.fromString(password).toString()).isEqualTo(password)); } } @Test void shouldWarnOnPasswordTooShort() { try (Neo4jContainer neo4jContainer = new Neo4jContainer("neo4j:4.4");) { Logger logger = (Logger) DockerLoggerFactory.getLogger("neo4j:4.4"); TestLogAppender testLogAppender = new TestLogAppender(); logger.addAppender(testLogAppender); testLogAppender.start(); neo4jContainer.withAdminPassword("short"); testLogAppender.stop(); assertThat(testLogAppender.passwordTooShortWarningAppeared).isTrue(); } } private static class CustomDummyWaitStrategy extends AbstractWaitStrategy { @Override protected void waitUntilReady() { // ehm...ready } } private static class TestLogAppender extends AppenderBase { boolean passwordTooShortWarningAppeared = false; @Override protected void append(ILoggingEvent eventObject) { if (eventObject.getLevel().equals(Level.WARN)) { if ( eventObject .getMessage() .equals("Your provided admin password is too short and will not work with Neo4j 5.3+.") ) { passwordTooShortWarningAppeared = true; } } } } private static Driver getDriver(Neo4jContainer container) { AuthToken authToken = AuthTokens.none(); if (container.getAdminPassword() != null) { authToken = AuthTokens.basic("neo4j", container.getAdminPassword()); } return GraphDatabase.driver(container.getBoltUrl(), authToken); } } ================================================ FILE: modules/neo4j/src/test/resources/example-container-license-acceptance.txt ================================================ neo4j:4.4.1-enterprise ================================================ FILE: modules/neo4j/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/nginx/build.gradle ================================================ description = "Testcontainers :: Nginx" dependencies { api project(':testcontainers') compileOnly 'org.jetbrains:annotations:26.0.2-1' } ================================================ FILE: modules/nginx/src/main/java/org/testcontainers/containers/NginxContainer.java ================================================ package org.testcontainers.containers; import org.jetbrains.annotations.NotNull; import org.testcontainers.containers.traits.LinkableContainer; import org.testcontainers.utility.DockerImageName; import java.net.MalformedURLException; import java.net.URL; import java.util.Set; /** * @deprecated use {@link org.testcontainers.nginx.NginxContainer} instead. */ @Deprecated public class NginxContainer> extends GenericContainer implements LinkableContainer { private static final int NGINX_DEFAULT_PORT = 80; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("nginx"); private static final String DEFAULT_TAG = "1.9.4"; /** * @deprecated use {@link #NginxContainer(DockerImageName)} instead */ @Deprecated public NginxContainer() { this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } public NginxContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public NginxContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); addExposedPort(NGINX_DEFAULT_PORT); setCommand("nginx", "-g", "daemon off;"); } /** * @return the ports on which to check if the container is ready * @deprecated use {@link #getLivenessCheckPortNumbers()} instead */ @NotNull @Override @Deprecated protected Set getLivenessCheckPorts() { return super.getLivenessCheckPorts(); } public URL getBaseUrl(String scheme, int port) throws MalformedURLException { return new URL(scheme + "://" + getHost() + ":" + getMappedPort(port)); } @Deprecated public void setCustomContent(String htmlContentPath) { addFileSystemBind(htmlContentPath, "/usr/share/nginx/html", BindMode.READ_ONLY); } @Deprecated public SELF withCustomContent(String htmlContentPath) { this.setCustomContent(htmlContentPath); return self(); } } ================================================ FILE: modules/nginx/src/main/java/org/testcontainers/nginx/NginxContainer.java ================================================ package org.testcontainers.nginx; import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.DockerImageName; import java.net.MalformedURLException; import java.net.URL; public class NginxContainer extends GenericContainer { private static final int NGINX_DEFAULT_PORT = 80; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("nginx"); public NginxContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public NginxContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); addExposedPort(NGINX_DEFAULT_PORT); setCommand("nginx", "-g", "daemon off;"); } public URL getBaseUrl(String scheme, int port) throws MalformedURLException { return new URL(scheme + "://" + getHost() + ":" + getMappedPort(port)); } public URL getBaseUrl(String scheme) throws MalformedURLException { return getBaseUrl(scheme, NGINX_DEFAULT_PORT); } } ================================================ FILE: modules/nginx/src/test/java/org/testcontainers/nginx/NginxContainerTest.java ================================================ package org.testcontainers.nginx; import lombok.Cleanup; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; import java.net.URL; import java.net.URLConnection; import static org.assertj.core.api.Assertions.assertThat; class NginxContainerTest { private static final DockerImageName NGINX_IMAGE = DockerImageName.parse("nginx:1.27.0-alpine3.19-slim"); private static String tmpDirectory = System.getProperty("user.home") + "/.tmp-test-container"; @SuppressWarnings({ "Duplicates", "ResultOfMethodCallIgnored" }) @BeforeAll static void setupContent() throws Exception { // addCustomContent { // Create a temporary dir File contentFolder = new File(tmpDirectory); contentFolder.mkdir(); contentFolder.deleteOnExit(); // And "hello world" HTTP file File indexFile = new File(contentFolder, "index.html"); indexFile.deleteOnExit(); @Cleanup PrintStream printStream = new PrintStream(new FileOutputStream(indexFile)); printStream.println("Hello World!"); // } } @Test void testSimple() throws Exception { try ( // creatingContainer { NginxContainer nginx = new NginxContainer(NGINX_IMAGE) .withCopyFileToContainer(MountableFile.forHostPath(tmpDirectory), "/usr/share/nginx/html") .waitingFor(new HttpWaitStrategy()); // } ) { nginx.start(); // getFromNginxServer { URL baseUrl = nginx.getBaseUrl("http", 80); assertThat(responseFromNginx(baseUrl)) .as("An HTTP GET from the Nginx server returns the index.html from the custom content directory") .contains("Hello World!"); // } assertHasCorrectExposedAndLivenessCheckPorts(nginx); } } private void assertHasCorrectExposedAndLivenessCheckPorts(NginxContainer nginxContainer) { assertThat(nginxContainer.getExposedPorts()).containsExactly(80); assertThat(nginxContainer.getLivenessCheckPortNumbers()).containsExactly(nginxContainer.getMappedPort(80)); } private static String responseFromNginx(URL baseUrl) throws IOException { URLConnection urlConnection = baseUrl.openConnection(); @Cleanup BufferedReader reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); return reader.readLine(); } } ================================================ FILE: modules/nginx/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/oceanbase/build.gradle ================================================ description = "Testcontainers :: JDBC :: OceanBase" dependencies { api project(':testcontainers-jdbc') testImplementation project(':testcontainers-jdbc-test') testRuntimeOnly 'com.mysql:mysql-connector-j:9.5.0' } ================================================ FILE: modules/oceanbase/src/main/java/org/testcontainers/oceanbase/OceanBaseCEContainer.java ================================================ package org.testcontainers.oceanbase; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; /** * Testcontainers implementation for OceanBase Community Edition. *

* Supported image: {@code oceanbase/oceanbase-ce} *

* Exposed ports: *

    *
  • SQL: 2881
  • *
  • RPC: 2882
  • *
*/ public class OceanBaseCEContainer extends JdbcDatabaseContainer { static final String NAME = "oceanbasece"; static final String DOCKER_IMAGE_NAME = "oceanbase/oceanbase-ce"; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(DOCKER_IMAGE_NAME); private static final Integer SQL_PORT = 2881; private static final Integer RPC_PORT = 2882; private static final String DEFAULT_TENANT_NAME = "test"; private static final String DEFAULT_USER = "root"; private static final String DEFAULT_PASSWORD = ""; private static final String DEFAULT_DATABASE_NAME = "test"; private Mode mode = Mode.SLIM; private String tenantName = DEFAULT_TENANT_NAME; private String password = DEFAULT_PASSWORD; public OceanBaseCEContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public OceanBaseCEContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); addExposedPorts(SQL_PORT, RPC_PORT); setWaitStrategy(Wait.forLogMessage(".*boot success!.*", 1)); } @Override protected void configure() { addEnv("MODE", mode.name().toLowerCase()); if (!DEFAULT_TENANT_NAME.equals(tenantName)) { if (mode == Mode.SLIM) { logger().warn("The tenant name is not configurable on slim mode, so this option will be ignored."); // reset the tenant name to ensure the constructed username is correct tenantName = DEFAULT_TENANT_NAME; } else { addEnv("OB_TENANT_NAME", tenantName); } } addEnv("OB_TENANT_PASSWORD", password); } @Override protected void waitUntilContainerStarted() { getWaitStrategy().waitUntilReady(this); } @Override public String getDriverClassName() { return OceanBaseJdbcUtils.getDriverClass(); } @Override public String getJdbcUrl() { String additionalUrlParams = constructUrlParameters("?", "&"); String prefix = OceanBaseJdbcUtils.isMySQLDriver(getDriverClassName()) ? "jdbc:mysql://" : "jdbc:oceanbase://"; return prefix + getHost() + ":" + getMappedPort(SQL_PORT) + "/" + DEFAULT_DATABASE_NAME + additionalUrlParams; } @Override public String getDatabaseName() { return DEFAULT_DATABASE_NAME; } @Override public String getUsername() { return DEFAULT_USER + "@" + tenantName; } @Override public String getPassword() { return password; } @Override protected String getTestQueryString() { return "SELECT 1"; } public OceanBaseCEContainer withMode(Mode mode) { this.mode = mode; return this; } public OceanBaseCEContainer withTenantName(String tenantName) { this.tenantName = tenantName; return this; } public OceanBaseCEContainer withPassword(String password) { this.password = password; return this; } public enum Mode { /** * Use as much hardware resources as possible for deployment by default, * and all environment variables are available. */ NORMAL, /** * Use the minimum hardware resources for deployment by default, * and all environment variables are available. */ MINI, /** * Use minimal hardware resources and pre-built deployment files for quick startup, * and password of user tenant is the only available environment variable. */ SLIM, } } ================================================ FILE: modules/oceanbase/src/main/java/org/testcontainers/oceanbase/OceanBaseCEContainerProvider.java ================================================ package org.testcontainers.oceanbase; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.containers.JdbcDatabaseContainerProvider; import org.testcontainers.utility.DockerImageName; /** * Factory for OceanBase Community Edition containers. */ public class OceanBaseCEContainerProvider extends JdbcDatabaseContainerProvider { private static final String DEFAULT_TAG = "4.2.1.8-108000022024072217"; @Override public boolean supports(String databaseType) { return databaseType.equals(OceanBaseCEContainer.NAME); } @Override public JdbcDatabaseContainer newInstance() { return newInstance(DEFAULT_TAG); } @Override public JdbcDatabaseContainer newInstance(String tag) { if (tag != null) { return new OceanBaseCEContainer(DockerImageName.parse(OceanBaseCEContainer.DOCKER_IMAGE_NAME).withTag(tag)); } else { return newInstance(); } } } ================================================ FILE: modules/oceanbase/src/main/java/org/testcontainers/oceanbase/OceanBaseJdbcUtils.java ================================================ package org.testcontainers.oceanbase; import java.util.Arrays; import java.util.List; /** * Utils for OceanBase Jdbc Connection. */ class OceanBaseJdbcUtils { static final String MYSQL_JDBC_DRIVER = "com.mysql.cj.jdbc.Driver"; static final String MYSQL_LEGACY_JDBC_DRIVER = "com.mysql.jdbc.Driver"; static final String OCEANBASE_JDBC_DRIVER = "com.oceanbase.jdbc.Driver"; static final String OCEANBASE_LEGACY_JDBC_DRIVER = "com.alipay.oceanbase.jdbc.Driver"; static final List SUPPORTED_DRIVERS = Arrays.asList( OCEANBASE_JDBC_DRIVER, OCEANBASE_LEGACY_JDBC_DRIVER, MYSQL_JDBC_DRIVER, MYSQL_LEGACY_JDBC_DRIVER ); static String getDriverClass() { for (String driverClass : SUPPORTED_DRIVERS) { try { Class.forName(driverClass); return driverClass; } catch (ClassNotFoundException e) { // try to load next driver } } throw new RuntimeException("Can't find valid driver class for OceanBase"); } static boolean isMySQLDriver(String driverClassName) { return MYSQL_JDBC_DRIVER.equals(driverClassName) || MYSQL_LEGACY_JDBC_DRIVER.equals(driverClassName); } static boolean isOceanBaseDriver(String driverClassName) { return OCEANBASE_JDBC_DRIVER.equals(driverClassName) || OCEANBASE_LEGACY_JDBC_DRIVER.equals(driverClassName); } } ================================================ FILE: modules/oceanbase/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider ================================================ org.testcontainers.oceanbase.OceanBaseCEContainerProvider ================================================ FILE: modules/oceanbase/src/test/java/org/testcontainers/oceanbase/OceanBaseJdbcDriverTest.java ================================================ package org.testcontainers.oceanbase; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; class OceanBaseJdbcDriverTest extends AbstractJDBCDriverTest { public static Iterable data() { return Arrays.asList( new Object[][] { { "jdbc:tc:oceanbasece://hostname/databasename", EnumSet.noneOf(Options.class) } } ); } } ================================================ FILE: modules/oceanbase/src/test/java/org/testcontainers/oceanbase/SimpleOceanBaseCETest.java ================================================ package org.testcontainers.oceanbase; import org.junit.jupiter.api.Test; import org.testcontainers.db.AbstractContainerDatabaseTest; import java.sql.ResultSet; import java.sql.SQLException; import static org.assertj.core.api.Assertions.assertThat; class SimpleOceanBaseCETest extends AbstractContainerDatabaseTest { private static final String IMAGE = "oceanbase/oceanbase-ce:4.2.1.8-108000022024072217"; @Test void testSimple() throws SQLException { try ( // container { OceanBaseCEContainer oceanbase = new OceanBaseCEContainer( "oceanbase/oceanbase-ce:4.2.1.8-108000022024072217" ) // } ) { oceanbase.start(); ResultSet resultSet = performQuery(oceanbase, "SELECT 1"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); assertHasCorrectExposedAndLivenessCheckPorts(oceanbase); } } @Test void testExplicitInitScript() throws SQLException { try (OceanBaseCEContainer oceanbase = new OceanBaseCEContainer(IMAGE).withInitScript("init.sql")) { oceanbase.start(); ResultSet resultSet = performQuery(oceanbase, "SELECT foo FROM bar"); String firstColumnValue = resultSet.getString(1); assertThat(firstColumnValue).as("Value from init script should equal real value").isEqualTo("hello world"); } } @Test void testWithAdditionalUrlParamInJdbcUrl() { try (OceanBaseCEContainer oceanbase = new OceanBaseCEContainer(IMAGE).withUrlParam("useSSL", "false")) { oceanbase.start(); String jdbcUrl = oceanbase.getJdbcUrl(); assertThat(jdbcUrl).contains("?"); assertThat(jdbcUrl).contains("useSSL=false"); } } private void assertHasCorrectExposedAndLivenessCheckPorts(OceanBaseCEContainer oceanbase) { int sqlPort = 2881; int rpcPort = 2882; assertThat(oceanbase.getExposedPorts()).containsExactlyInAnyOrder(sqlPort, rpcPort); assertThat(oceanbase.getLivenessCheckPortNumbers()) .containsExactlyInAnyOrder(oceanbase.getMappedPort(sqlPort), oceanbase.getMappedPort(rpcPort)); } } ================================================ FILE: modules/oceanbase/src/test/resources/init.sql ================================================ CREATE TABLE bar ( foo VARCHAR(255) ); DROP PROCEDURE IF EXISTS -- ; count_foo; SELECT "a /* string literal containing comment characters like -- here"; SELECT "a 'quoting' \"scenario ` involving BEGIN keyword\" here"; SELECT * from `bar`; -- What about a line comment containing imbalanced string delimiters? " CREATE PROCEDURE count_foo() BEGIN BEGIN SELECT * FROM bar; SELECT 1 FROM dual; END; BEGIN select * from bar; END; -- we can do comments /* including block comments */ /* what if BEGIN appears inside a comment? */ select "or what if BEGIN appears inside a literal?"; END /*; */; /* or a block comment containing imbalanced string delimiters? ' " */ INSERT INTO bar (foo) /* ; */ VALUES ('hello world'); ================================================ FILE: modules/oceanbase/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/ollama/build.gradle ================================================ description = "Testcontainers :: Ollama" dependencies { api project(':testcontainers') testImplementation 'io.rest-assured:rest-assured:5.5.6' } ================================================ FILE: modules/ollama/src/main/java/org/testcontainers/ollama/OllamaContainer.java ================================================ package org.testcontainers.ollama; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.model.DeviceRequest; import com.github.dockerjava.api.model.Image; import com.github.dockerjava.api.model.Info; import com.github.dockerjava.api.model.RuntimeInfo; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.DockerImageName; import java.util.Collections; import java.util.List; import java.util.Map; /** * Testcontainers implementation for Ollama. *

* Supported image: {@code ollama/ollama} *

* Exposed ports: 11434 */ public class OllamaContainer extends GenericContainer { private static final DockerImageName DOCKER_IMAGE_NAME = DockerImageName.parse("ollama/ollama"); private static final int OLLAMA_PORT = 11434; public OllamaContainer(String image) { this(DockerImageName.parse(image)); } public OllamaContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DOCKER_IMAGE_NAME); Info info = this.dockerClient.infoCmd().exec(); Map runtimes = info.getRuntimes(); if (runtimes != null) { if (runtimes.containsKey("nvidia")) { withCreateContainerCmdModifier(cmd -> { cmd .getHostConfig() .withDeviceRequests( Collections.singletonList( new DeviceRequest() .withCapabilities(Collections.singletonList(Collections.singletonList("gpu"))) .withCount(-1) ) ); }); } } withExposedPorts(OLLAMA_PORT); } /** * Commits the current file system changes in the container into a new image. * Should be used for creating an image that contains a loaded model. * @param imageName the name of the new image */ public void commitToImage(String imageName) { DockerImageName dockerImageName = DockerImageName.parse(getDockerImageName()); if (!dockerImageName.equals(DockerImageName.parse(imageName))) { DockerClient dockerClient = DockerClientFactory.instance().client(); List images = dockerClient.listImagesCmd().withReferenceFilter(imageName).exec(); if (images.isEmpty()) { DockerImageName imageModel = DockerImageName.parse(imageName); dockerClient .commitCmd(getContainerId()) .withRepository(imageModel.getUnversionedPart()) .withLabels(Collections.singletonMap("org.testcontainers.sessionId", "")) .withTag(imageModel.getVersionPart()) .exec(); } } } public int getPort() { return getMappedPort(OLLAMA_PORT); } public String getEndpoint() { return "http://" + getHost() + ":" + getPort(); } } ================================================ FILE: modules/ollama/src/test/java/org/testcontainers/ollama/OllamaContainerTest.java ================================================ package org.testcontainers.ollama; import org.junit.jupiter.api.Test; import org.testcontainers.utility.Base58; import org.testcontainers.utility.DockerImageName; import java.io.IOException; import static io.restassured.RestAssured.given; import static org.assertj.core.api.Assertions.assertThat; class OllamaContainerTest { @Test void withDefaultConfig() { try ( // container { OllamaContainer ollama = new OllamaContainer("ollama/ollama:0.1.26") // } ) { ollama.start(); String version = given().baseUri(ollama.getEndpoint()).get("/api/version").jsonPath().get("version"); assertThat(version).isEqualTo("0.1.26"); } } @Test void downloadModelAndCommitToImage() throws IOException, InterruptedException { String newImageName = "tc-ollama-allminilm-" + Base58.randomString(4).toLowerCase(); try (OllamaContainer ollama = new OllamaContainer("ollama/ollama:0.1.26")) { ollama.start(); // pullModel { ollama.execInContainer("ollama", "pull", "all-minilm"); // } String modelName = given() .baseUri(ollama.getEndpoint()) .get("/api/tags") .jsonPath() .getString("models[0].name"); assertThat(modelName).contains("all-minilm"); // commitToImage { ollama.commitToImage(newImageName); // } } try ( // spotless:off // substitute { OllamaContainer ollama = new OllamaContainer( DockerImageName.parse(newImageName) .asCompatibleSubstituteFor("ollama/ollama") ) // } // spotless:on ) { ollama.start(); String modelName = given() .baseUri(ollama.getEndpoint()) .get("/api/tags") .jsonPath() .getString("models[0].name"); assertThat(modelName).contains("all-minilm"); } } } ================================================ FILE: modules/ollama/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/openfga/build.gradle ================================================ description = "Testcontainers :: OpenFGA" dependencies { api project(':testcontainers') testImplementation 'dev.openfga:openfga-sdk:0.9.4' } test { javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(17) } } compileTestJava { javaCompiler = javaToolchains.compilerFor { languageVersion = JavaLanguageVersion.of(17) } options.release.set(17) } ================================================ FILE: modules/openfga/src/main/java/org/testcontainers/openfga/OpenFGAContainer.java ================================================ package org.testcontainers.openfga; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; /** * Testcontainers implementation for OpenFGA. *

* Supported image: {@code openfga/openfga} *

* Exposed ports: *

    *
  • Playground: 3000
  • *
  • HTTP: 8080
  • *
  • gRPC: 8081
  • *
*/ public class OpenFGAContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("openfga/openfga"); public OpenFGAContainer(String image) { this(DockerImageName.parse(image)); } public OpenFGAContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); withExposedPorts(3000, 8080, 8081); withCommand("run"); waitingFor( Wait.forHttp("/healthz").forPort(8080).forResponsePredicate(response -> response.contains("SERVING")) ); } public String getHttpEndpoint() { return "http://" + getHost() + ":" + getMappedPort(8080); } public String getGrpcEndpoint() { return "http://" + getHost() + ":" + getMappedPort(8081); } } ================================================ FILE: modules/openfga/src/test/java/org/testcontainers/openfga/OpenFGAContainerTest.java ================================================ package org.testcontainers.openfga; import dev.openfga.sdk.api.client.OpenFgaClient; import dev.openfga.sdk.api.client.model.ClientCreateStoreResponse; import dev.openfga.sdk.api.configuration.ClientConfiguration; import dev.openfga.sdk.api.model.CreateStoreRequest; import dev.openfga.sdk.errors.FgaInvalidParameterException; import org.junit.jupiter.api.Test; import java.util.concurrent.ExecutionException; import static org.assertj.core.api.Assertions.assertThat; class OpenFGAContainerTest { @Test void withDefaultConfig() throws FgaInvalidParameterException, ExecutionException, InterruptedException { try ( // container { OpenFGAContainer openfga = new OpenFGAContainer("openfga/openfga:v1.4.3") // } ) { openfga.start(); ClientConfiguration config = new ClientConfiguration().apiUrl(openfga.getHttpEndpoint()); OpenFgaClient client = new OpenFgaClient(config); assertThat(client.listStores().get().getStores()).isEmpty(); ClientCreateStoreResponse store = client.createStore(new CreateStoreRequest().name("test")).get(); assertThat(store.getId()).isNotNull(); assertThat(client.listStores().get().getStores()).hasSize(1); } } } ================================================ FILE: modules/openfga/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/oracle-free/build.gradle ================================================ description = "Testcontainers :: JDBC :: Oracle Database Free" dependencies { api project(':testcontainers-jdbc') compileOnly project(':testcontainers-r2dbc') compileOnly 'com.oracle.database.r2dbc:oracle-r2dbc:1.3.0' testImplementation project(':testcontainers-jdbc-test') testImplementation 'com.oracle.database.jdbc:ojdbc11:23.26.0.0.0' compileOnly 'org.jetbrains:annotations:26.0.2-1' testImplementation testFixtures(project(':testcontainers-r2dbc')) testRuntimeOnly 'com.oracle.database.r2dbc:oracle-r2dbc:1.3.0' } ================================================ FILE: modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleContainer.java ================================================ package org.testcontainers.oracle; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import java.time.Duration; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Set; /** * Testcontainers implementation for Oracle Database Free. *

* Supported image: {@code gvenzl/oracle-free} *

* Exposed ports: 1521 */ public class OracleContainer extends JdbcDatabaseContainer { static final String NAME = "oracle"; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("gvenzl/oracle-free"); static final String DEFAULT_TAG = "slim"; static final String IMAGE = DEFAULT_IMAGE_NAME.getUnversionedPart(); static final int ORACLE_PORT = 1521; private static final int DEFAULT_STARTUP_TIMEOUT_SECONDS = 60; private static final int DEFAULT_CONNECT_TIMEOUT_SECONDS = 60; // Container defaults static final String DEFAULT_DATABASE_NAME = "freepdb1"; static final String DEFAULT_SID = "free"; static final String DEFAULT_SYSTEM_USER = "system"; static final String DEFAULT_SYS_USER = "sys"; // Test container defaults static final String APP_USER = "test"; static final String APP_USER_PASSWORD = "test"; // Restricted user and database names private static final List ORACLE_SYSTEM_USERS = Arrays.asList(DEFAULT_SYSTEM_USER, DEFAULT_SYS_USER); private String databaseName = DEFAULT_DATABASE_NAME; private String username = APP_USER; private String password = APP_USER_PASSWORD; private boolean usingSid = false; public OracleContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public OracleContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); waitingFor( Wait .forLogMessage(".*DATABASE IS READY TO USE!.*\\s", 1) .withStartupTimeout(Duration.ofSeconds(DEFAULT_STARTUP_TIMEOUT_SECONDS)) ); withConnectTimeoutSeconds(DEFAULT_CONNECT_TIMEOUT_SECONDS); addExposedPorts(ORACLE_PORT); } @Override protected void waitUntilContainerStarted() { getWaitStrategy().waitUntilReady(this); } @NotNull @Override public Set getLivenessCheckPortNumbers() { return Collections.singleton(getMappedPort(ORACLE_PORT)); } @Override public String getDriverClassName() { try { Class.forName("oracle.jdbc.OracleDriver"); return "oracle.jdbc.OracleDriver"; } catch (ClassNotFoundException e) { return "oracle.jdbc.driver.OracleDriver"; } } @Override public String getJdbcUrl() { return isUsingSid() ? "jdbc:oracle:thin:" + "@" + getHost() + ":" + getOraclePort() + ":" + getSid() : "jdbc:oracle:thin:" + "@" + getHost() + ":" + getOraclePort() + "/" + getDatabaseName(); } @Override public String getUsername() { // An application user is tied to the database, and therefore not authenticated to connect to SID. return isUsingSid() ? DEFAULT_SYSTEM_USER : username; } @Override public String getPassword() { return password; } @Override public String getDatabaseName() { return databaseName; } protected boolean isUsingSid() { return usingSid; } @Override public OracleContainer withUsername(String username) { if (StringUtils.isEmpty(username)) { throw new IllegalArgumentException("Username cannot be null or empty"); } if (ORACLE_SYSTEM_USERS.contains(username.toLowerCase())) { throw new IllegalArgumentException("Username cannot be one of " + ORACLE_SYSTEM_USERS); } this.username = username; return self(); } @Override public OracleContainer withPassword(String password) { if (StringUtils.isEmpty(password)) { throw new IllegalArgumentException("Password cannot be null or empty"); } this.password = password; return self(); } @Override public OracleContainer withDatabaseName(String databaseName) { if (StringUtils.isEmpty(databaseName)) { throw new IllegalArgumentException("Database name cannot be null or empty"); } if (DEFAULT_DATABASE_NAME.equals(databaseName.toLowerCase())) { throw new IllegalArgumentException("Database name cannot be set to " + DEFAULT_DATABASE_NAME); } this.databaseName = databaseName; return self(); } public OracleContainer usingSid() { this.usingSid = true; return self(); } @Override public OracleContainer withUrlParam(String paramName, String paramValue) { throw new UnsupportedOperationException("The Oracle Database driver does not support this"); } @SuppressWarnings("SameReturnValue") public String getSid() { return DEFAULT_SID; } public Integer getOraclePort() { return getMappedPort(ORACLE_PORT); } @Override public String getTestQueryString() { return "SELECT 1 FROM DUAL"; } @Override protected void configure() { withEnv("ORACLE_PASSWORD", password); // Only set ORACLE_DATABASE if different than the default. if (databaseName != DEFAULT_DATABASE_NAME) { withEnv("ORACLE_DATABASE", databaseName); } withEnv("APP_USER", username); withEnv("APP_USER_PASSWORD", password); } } ================================================ FILE: modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleContainerProvider.java ================================================ package org.testcontainers.oracle; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.containers.JdbcDatabaseContainerProvider; import org.testcontainers.utility.DockerImageName; /** * Factory for Oracle containers. */ public class OracleContainerProvider extends JdbcDatabaseContainerProvider { @Override public boolean supports(String databaseType) { return databaseType.equals(OracleContainer.NAME); } @Override public JdbcDatabaseContainer newInstance() { return newInstance(OracleContainer.DEFAULT_TAG); } @Override public JdbcDatabaseContainer newInstance(String tag) { if (tag != null) { return new OracleContainer(DockerImageName.parse(OracleContainer.IMAGE).withTag(tag)); } return newInstance(); } } ================================================ FILE: modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleR2DBCDatabaseContainer.java ================================================ package org.testcontainers.oracle; import io.r2dbc.spi.ConnectionFactoryOptions; import org.testcontainers.lifecycle.Startable; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; import java.util.Set; public class OracleR2DBCDatabaseContainer implements R2DBCDatabaseContainer { private final OracleContainer container; public OracleR2DBCDatabaseContainer(OracleContainer container) { this.container = container; } public static ConnectionFactoryOptions getOptions(OracleContainer container) { ConnectionFactoryOptions options = ConnectionFactoryOptions .builder() .option(ConnectionFactoryOptions.DRIVER, OracleR2DBCDatabaseContainerProvider.DRIVER) .build(); return new OracleR2DBCDatabaseContainer(container).configure(options); } @Override public ConnectionFactoryOptions configure(ConnectionFactoryOptions options) { return options .mutate() .option(ConnectionFactoryOptions.HOST, container.getHost()) .option(ConnectionFactoryOptions.PORT, container.getMappedPort(OracleContainer.ORACLE_PORT)) .option(ConnectionFactoryOptions.DATABASE, container.getDatabaseName()) .option(ConnectionFactoryOptions.USER, container.getUsername()) .option(ConnectionFactoryOptions.PASSWORD, container.getPassword()) .build(); } @Override public Set getDependencies() { return this.container.getDependencies(); } @Override public void start() { this.container.start(); } @Override public void stop() { this.container.stop(); } @Override public void close() { this.container.close(); } } ================================================ FILE: modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleR2DBCDatabaseContainerProvider.java ================================================ package org.testcontainers.oracle; 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; public class OracleR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider { static final String DRIVER = "oracle"; @Override public boolean supports(ConnectionFactoryOptions options) { return DRIVER.equals(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)); } @Override public R2DBCDatabaseContainer createContainer(ConnectionFactoryOptions options) { String image = OracleContainer.IMAGE + ":" + options.getRequiredValue(IMAGE_TAG_OPTION); OracleContainer container = new OracleContainer(image) .withDatabaseName((String) options.getRequiredValue(ConnectionFactoryOptions.DATABASE)); if (Boolean.TRUE.equals(options.getValue(REUSABLE_OPTION))) { container.withReuse(true); } return new OracleR2DBCDatabaseContainer(container); } @Nullable @Override public ConnectionFactoryMetadata getMetadata(ConnectionFactoryOptions options) { ConnectionFactoryOptions.Builder builder = options.mutate(); if (!options.hasOption(ConnectionFactoryOptions.USER)) { builder.option(ConnectionFactoryOptions.USER, OracleContainer.APP_USER); } if (!options.hasOption(ConnectionFactoryOptions.PASSWORD)) { builder.option(ConnectionFactoryOptions.PASSWORD, OracleContainer.APP_USER_PASSWORD); } return R2DBCDatabaseContainerProvider.super.getMetadata(builder.build()); } } ================================================ FILE: modules/oracle-free/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider ================================================ org.testcontainers.oracle.OracleContainerProvider ================================================ FILE: modules/oracle-free/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider ================================================ org.testcontainers.oracle.OracleR2DBCDatabaseContainerProvider ================================================ FILE: modules/oracle-free/src/test/java/org/testcontainers/junit/oracle/SimpleOracleTest.java ================================================ package org.testcontainers.junit.oracle; import org.junit.jupiter.api.Test; import org.testcontainers.db.AbstractContainerDatabaseTest; import org.testcontainers.oracle.OracleContainer; import org.testcontainers.utility.DockerImageName; import java.sql.ResultSet; import java.sql.SQLException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; class SimpleOracleTest extends AbstractContainerDatabaseTest { private static final DockerImageName ORACLE_DOCKER_IMAGE_NAME = DockerImageName.parse( "gvenzl/oracle-free:slim-faststart" ); private void runTest(OracleContainer container, String databaseName, String username, String password) throws SQLException { //Test config was honored assertThat(container.getDatabaseName()).isEqualTo(databaseName); assertThat(container.getUsername()).isEqualTo(username); assertThat(container.getPassword()).isEqualTo(password); //Test we can get a connection container.start(); ResultSet resultSet = performQuery(container, "SELECT 1 FROM dual"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); } @Test void testDefaultSettings() throws SQLException { try ( // container { OracleContainer oracle = new OracleContainer("gvenzl/oracle-free:slim-faststart") // } ) { runTest(oracle, "freepdb1", "test", "test"); // Match against the last '/' String urlSuffix = oracle.getJdbcUrl().split("(\\/)(?!.*\\/)", 2)[1]; assertThat(urlSuffix).isEqualTo("freepdb1"); } } @Test void testPluggableDatabase() throws SQLException { try (OracleContainer oracle = new OracleContainer(ORACLE_DOCKER_IMAGE_NAME).withDatabaseName("testDB")) { runTest(oracle, "testDB", "test", "test"); } } @Test void testPluggableDatabaseAndCustomUser() throws SQLException { try ( OracleContainer oracle = new OracleContainer("gvenzl/oracle-free:slim-faststart") .withDatabaseName("testDB") .withUsername("testUser") .withPassword("testPassword") ) { runTest(oracle, "testDB", "testUser", "testPassword"); } } @Test void testCustomUser() throws SQLException { try ( OracleContainer oracle = new OracleContainer(ORACLE_DOCKER_IMAGE_NAME) .withUsername("testUser") .withPassword("testPassword") ) { runTest(oracle, "freepdb1", "testUser", "testPassword"); } } @Test void testSID() throws SQLException { try (OracleContainer oracle = new OracleContainer(ORACLE_DOCKER_IMAGE_NAME).usingSid()) { runTest(oracle, "freepdb1", "system", "test"); // Match against the last ':' String urlSuffix = oracle.getJdbcUrl().split("(\\:)(?!.*\\:)", 2)[1]; assertThat(urlSuffix).isEqualTo("free"); } } @Test void testSIDAndCustomPassword() throws SQLException { try ( OracleContainer oracle = new OracleContainer(ORACLE_DOCKER_IMAGE_NAME) .usingSid() .withPassword("testPassword") ) { runTest(oracle, "freepdb1", "system", "testPassword"); } } @Test void testErrorPaths() throws SQLException { try (OracleContainer oracle = new OracleContainer(ORACLE_DOCKER_IMAGE_NAME)) { try { oracle.withDatabaseName("FREEPDB1"); fail("Should not have been able to set database name to freepdb1."); } catch (IllegalArgumentException e) { //expected } try { oracle.withDatabaseName(""); fail("Should not have been able to set database name to nothing."); } catch (IllegalArgumentException e) { //expected } try { oracle.withUsername("SYSTEM"); fail("Should not have been able to set username to system."); } catch (IllegalArgumentException e) { //expected } try { oracle.withUsername("SYS"); fail("Should not have been able to set username to sys."); } catch (IllegalArgumentException e) { //expected } try { oracle.withUsername(""); fail("Should not have been able to set username to nothing."); } catch (IllegalArgumentException e) { //expected } try { oracle.withPassword(""); fail("Should not have been able to set password to nothing."); } catch (IllegalArgumentException e) { //expected } } } } ================================================ FILE: modules/oracle-free/src/test/java/org/testcontainers/oracle/jdbc/OracleJDBCDriverTest.java ================================================ package org.testcontainers.oracle.jdbc; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.ResultSetHandler; import org.junit.jupiter.api.Test; import java.sql.ResultSet; import java.sql.SQLException; import static org.assertj.core.api.Assertions.assertThat; class OracleJDBCDriverTest { @Test void testOracleWithNoSpecifiedVersion() throws SQLException { performSimpleTest("jdbc:tc:oracle://hostname/databasename"); } private void performSimpleTest(String jdbcUrl) throws SQLException { HikariDataSource dataSource = getDataSource(jdbcUrl, 1); new QueryRunner(dataSource) .query( "SELECT 1 FROM dual", new ResultSetHandler() { @Override public Object handle(ResultSet rs) throws SQLException { rs.next(); int resultSetInt = rs.getInt(1); assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); return true; } } ); dataSource.close(); } private HikariDataSource getDataSource(String jdbcUrl, int poolSize) { HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setJdbcUrl(jdbcUrl); hikariConfig.setConnectionTestQuery("SELECT 1 FROM dual"); hikariConfig.setMinimumIdle(1); hikariConfig.setMaximumPoolSize(poolSize); return new HikariDataSource(hikariConfig); } } ================================================ FILE: modules/oracle-free/src/test/java/org/testcontainers/oracle/r2dbc/OracleR2DBCDatabaseContainerTest.java ================================================ package org.testcontainers.oracle.r2dbc; import io.r2dbc.spi.ConnectionFactoryOptions; import org.testcontainers.oracle.OracleContainer; import org.testcontainers.oracle.OracleR2DBCDatabaseContainer; import org.testcontainers.r2dbc.AbstractR2DBCDatabaseContainerTest; public class OracleR2DBCDatabaseContainerTest extends AbstractR2DBCDatabaseContainerTest { @Override protected OracleContainer createContainer() { return new OracleContainer("gvenzl/oracle-free:slim-faststart"); } @Override protected ConnectionFactoryOptions getOptions(OracleContainer container) { ConnectionFactoryOptions options = OracleR2DBCDatabaseContainer.getOptions(container); return options; } protected String createR2DBCUrl() { return "r2dbc:tc:oracle:///db?TC_IMAGE_TAG=slim-faststart"; } @Override protected String query() { return "SELECT %s from dual"; } } ================================================ FILE: modules/oracle-free/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/oracle-xe/build.gradle ================================================ description = "Testcontainers :: JDBC :: Oracle XE" dependencies { api project(':testcontainers-jdbc') compileOnly project(':testcontainers-r2dbc') compileOnly 'com.oracle.database.r2dbc:oracle-r2dbc:1.3.0' testImplementation project(':testcontainers-jdbc-test') testImplementation 'com.oracle.database.jdbc:ojdbc11:23.26.0.0.0' compileOnly 'org.jetbrains:annotations:26.0.2-1' testImplementation testFixtures(project(':testcontainers-r2dbc')) testRuntimeOnly 'com.oracle.database.r2dbc:oracle-r2dbc:1.3.0' } ================================================ FILE: modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleContainer.java ================================================ package org.testcontainers.containers; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; import org.testcontainers.utility.DockerImageName; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.Future; /** * Testcontainers implementation for Oracle. *

* Supported image: {@code gvenzl/oracle-xe} *

* Exposed ports: 1521 */ public class OracleContainer extends JdbcDatabaseContainer { public static final String NAME = "oracle"; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("gvenzl/oracle-xe"); static final String DEFAULT_TAG = "18.4.0-slim"; static final String IMAGE = DEFAULT_IMAGE_NAME.getUnversionedPart(); static final int ORACLE_PORT = 1521; private static final int APEX_HTTP_PORT = 8080; private static final int DEFAULT_STARTUP_TIMEOUT_SECONDS = 240; private static final int DEFAULT_CONNECT_TIMEOUT_SECONDS = 120; // Container defaults static final String DEFAULT_DATABASE_NAME = "xepdb1"; static final String DEFAULT_SID = "xe"; static final String DEFAULT_SYSTEM_USER = "system"; static final String DEFAULT_SYS_USER = "sys"; // Test container defaults static final String APP_USER = "test"; static final String APP_USER_PASSWORD = "test"; // Restricted user and database names private static final List ORACLE_SYSTEM_USERS = Arrays.asList(DEFAULT_SYSTEM_USER, DEFAULT_SYS_USER); private String databaseName = DEFAULT_DATABASE_NAME; private String username = APP_USER; private String password = APP_USER_PASSWORD; private boolean usingSid = false; /** * @deprecated use {@link #OracleContainer(DockerImageName)} instead */ @Deprecated public OracleContainer() { this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } public OracleContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public OracleContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); preconfigure(); } public OracleContainer(Future dockerImageName) { super(dockerImageName); preconfigure(); } private void preconfigure() { this.waitStrategy = new LogMessageWaitStrategy() .withRegEx(".*DATABASE IS READY TO USE!.*\\s") .withTimes(1) .withStartupTimeout(Duration.of(DEFAULT_STARTUP_TIMEOUT_SECONDS, ChronoUnit.SECONDS)); withConnectTimeoutSeconds(DEFAULT_CONNECT_TIMEOUT_SECONDS); addExposedPorts(ORACLE_PORT, APEX_HTTP_PORT); } @Override protected void waitUntilContainerStarted() { getWaitStrategy().waitUntilReady(this); } @NotNull @Override public Set getLivenessCheckPortNumbers() { return Collections.singleton(getMappedPort(ORACLE_PORT)); } @Override public String getDriverClassName() { try { Class.forName("oracle.jdbc.OracleDriver"); return "oracle.jdbc.OracleDriver"; } catch (ClassNotFoundException e) { return "oracle.jdbc.driver.OracleDriver"; } } @Override public String getJdbcUrl() { return isUsingSid() ? "jdbc:oracle:thin:" + "@" + getHost() + ":" + getOraclePort() + ":" + getSid() : "jdbc:oracle:thin:" + "@" + getHost() + ":" + getOraclePort() + "/" + getDatabaseName(); } @Override public String getUsername() { // An application user is tied to the database, and therefore not authenticated to connect to SID. return isUsingSid() ? DEFAULT_SYSTEM_USER : username; } @Override public String getPassword() { return password; } @Override public String getDatabaseName() { return databaseName; } protected boolean isUsingSid() { return usingSid; } @Override public OracleContainer withUsername(String username) { if (StringUtils.isEmpty(username)) { throw new IllegalArgumentException("Username cannot be null or empty"); } if (ORACLE_SYSTEM_USERS.contains(username.toLowerCase())) { throw new IllegalArgumentException("Username cannot be one of " + ORACLE_SYSTEM_USERS); } this.username = username; return self(); } @Override public OracleContainer withPassword(String password) { if (StringUtils.isEmpty(password)) { throw new IllegalArgumentException("Password cannot be null or empty"); } this.password = password; return self(); } @Override public OracleContainer withDatabaseName(String databaseName) { if (StringUtils.isEmpty(databaseName)) { throw new IllegalArgumentException("Database name cannot be null or empty"); } if (DEFAULT_DATABASE_NAME.equals(databaseName.toLowerCase())) { throw new IllegalArgumentException("Database name cannot be set to " + DEFAULT_DATABASE_NAME); } this.databaseName = databaseName; return self(); } public OracleContainer usingSid() { this.usingSid = true; return self(); } @Override public OracleContainer withUrlParam(String paramName, String paramValue) { throw new UnsupportedOperationException("The Oracle Database driver does not support this"); } @SuppressWarnings("SameReturnValue") public String getSid() { return DEFAULT_SID; } public Integer getOraclePort() { return getMappedPort(ORACLE_PORT); } @SuppressWarnings("unused") public Integer getWebPort() { return getMappedPort(APEX_HTTP_PORT); } @Override public String getTestQueryString() { return "SELECT 1 FROM DUAL"; } @Override protected void configure() { withEnv("ORACLE_PASSWORD", password); // Only set ORACLE_DATABASE if different than the default. if (databaseName != DEFAULT_DATABASE_NAME) { withEnv("ORACLE_DATABASE", databaseName); } withEnv("APP_USER", username); withEnv("APP_USER_PASSWORD", password); } } ================================================ FILE: modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleContainerProvider.java ================================================ package org.testcontainers.containers; import org.testcontainers.utility.DockerImageName; /** * Factory for Oracle containers. */ public class OracleContainerProvider extends JdbcDatabaseContainerProvider { @Override public boolean supports(String databaseType) { return databaseType.equals(OracleContainer.NAME); } @Override public JdbcDatabaseContainer newInstance() { return newInstance(OracleContainer.DEFAULT_TAG); } @Override public JdbcDatabaseContainer newInstance(String tag) { if (tag != null) { return new OracleContainer(DockerImageName.parse(OracleContainer.IMAGE).withTag(tag)); } return newInstance(); } } ================================================ FILE: modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleR2DBCDatabaseContainer.java ================================================ package org.testcontainers.containers; import io.r2dbc.spi.ConnectionFactoryOptions; import lombok.RequiredArgsConstructor; import lombok.experimental.Delegate; import org.testcontainers.lifecycle.Startable; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; @RequiredArgsConstructor public class OracleR2DBCDatabaseContainer implements R2DBCDatabaseContainer { @Delegate(types = Startable.class) private final OracleContainer container; public static ConnectionFactoryOptions getOptions(OracleContainer container) { ConnectionFactoryOptions options = ConnectionFactoryOptions .builder() .option(ConnectionFactoryOptions.DRIVER, OracleR2DBCDatabaseContainerProvider.DRIVER) .build(); return new OracleR2DBCDatabaseContainer(container).configure(options); } @Override public ConnectionFactoryOptions configure(ConnectionFactoryOptions options) { return options .mutate() .option(ConnectionFactoryOptions.HOST, container.getHost()) .option(ConnectionFactoryOptions.PORT, container.getMappedPort(OracleContainer.ORACLE_PORT)) .option(ConnectionFactoryOptions.DATABASE, container.getDatabaseName()) .option(ConnectionFactoryOptions.USER, container.getUsername()) .option(ConnectionFactoryOptions.PASSWORD, container.getPassword()) .build(); } } ================================================ FILE: modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleR2DBCDatabaseContainerProvider.java ================================================ package org.testcontainers.containers; 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; public class OracleR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider { static final String DRIVER = "oracle"; @Override public boolean supports(ConnectionFactoryOptions options) { return DRIVER.equals(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)); } @Override public R2DBCDatabaseContainer createContainer(ConnectionFactoryOptions options) { String image = OracleContainer.IMAGE + ":" + options.getRequiredValue(IMAGE_TAG_OPTION); OracleContainer container = new OracleContainer(image) .withDatabaseName((String) options.getRequiredValue(ConnectionFactoryOptions.DATABASE)); if (Boolean.TRUE.equals(options.getValue(REUSABLE_OPTION))) { container.withReuse(true); } return new OracleR2DBCDatabaseContainer(container); } @Nullable @Override public ConnectionFactoryMetadata getMetadata(ConnectionFactoryOptions options) { ConnectionFactoryOptions.Builder builder = options.mutate(); if (!options.hasOption(ConnectionFactoryOptions.USER)) { builder.option(ConnectionFactoryOptions.USER, OracleContainer.APP_USER); } if (!options.hasOption(ConnectionFactoryOptions.PASSWORD)) { builder.option(ConnectionFactoryOptions.PASSWORD, OracleContainer.APP_USER_PASSWORD); } return R2DBCDatabaseContainerProvider.super.getMetadata(builder.build()); } } ================================================ FILE: modules/oracle-xe/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider ================================================ org.testcontainers.containers.OracleContainerProvider ================================================ FILE: modules/oracle-xe/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider ================================================ org.testcontainers.containers.OracleR2DBCDatabaseContainerProvider ================================================ FILE: modules/oracle-xe/src/test/java/org/testcontainers/containers/jdbc/OracleJDBCDriverTest.java ================================================ package org.testcontainers.containers.jdbc; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.ResultSetHandler; import org.junit.jupiter.api.Test; import java.sql.ResultSet; import java.sql.SQLException; import static org.assertj.core.api.Assertions.assertThat; class OracleJDBCDriverTest { @Test void testOracleWithNoSpecifiedVersion() throws SQLException { performSimpleTest("jdbc:tc:oracle://hostname/databasename"); } private void performSimpleTest(String jdbcUrl) throws SQLException { HikariDataSource dataSource = getDataSource(jdbcUrl, 1); new QueryRunner(dataSource) .query( "SELECT 1 FROM dual", new ResultSetHandler() { @Override public Object handle(ResultSet rs) throws SQLException { rs.next(); int resultSetInt = rs.getInt(1); assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); return true; } } ); dataSource.close(); } private HikariDataSource getDataSource(String jdbcUrl, int poolSize) { HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setJdbcUrl(jdbcUrl); hikariConfig.setConnectionTestQuery("SELECT 1 FROM dual"); hikariConfig.setMinimumIdle(1); hikariConfig.setMaximumPoolSize(poolSize); return new HikariDataSource(hikariConfig); } } ================================================ FILE: modules/oracle-xe/src/test/java/org/testcontainers/containers/r2dbc/OracleR2DBCDatabaseContainerTest.java ================================================ package org.testcontainers.containers.r2dbc; import io.r2dbc.spi.ConnectionFactoryOptions; import org.testcontainers.containers.OracleContainer; import org.testcontainers.containers.OracleR2DBCDatabaseContainer; import org.testcontainers.r2dbc.AbstractR2DBCDatabaseContainerTest; public class OracleR2DBCDatabaseContainerTest extends AbstractR2DBCDatabaseContainerTest { @Override protected OracleContainer createContainer() { return new OracleContainer("gvenzl/oracle-xe:21-slim-faststart"); } @Override protected ConnectionFactoryOptions getOptions(OracleContainer container) { ConnectionFactoryOptions options = OracleR2DBCDatabaseContainer.getOptions(container); return options; } protected String createR2DBCUrl() { return "r2dbc:tc:oracle:///db?TC_IMAGE_TAG=21-slim-faststart"; } @Override protected String query() { return "SELECT %s from dual"; } } ================================================ FILE: modules/oracle-xe/src/test/java/org/testcontainers/junit/oracle/SimpleOracleTest.java ================================================ package org.testcontainers.junit.oracle; import org.junit.jupiter.api.Test; import org.testcontainers.containers.OracleContainer; import org.testcontainers.db.AbstractContainerDatabaseTest; import org.testcontainers.utility.DockerImageName; import java.sql.ResultSet; import java.sql.SQLException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; class SimpleOracleTest extends AbstractContainerDatabaseTest { public static final DockerImageName ORACLE_DOCKER_IMAGE_NAME = DockerImageName.parse( "gvenzl/oracle-xe:21-slim-faststart" ); private void runTest(OracleContainer container, String databaseName, String username, String password) throws SQLException { //Test config was honored assertThat(container.getDatabaseName()).isEqualTo(databaseName); assertThat(container.getUsername()).isEqualTo(username); assertThat(container.getPassword()).isEqualTo(password); //Test we can get a connection container.start(); ResultSet resultSet = performQuery(container, "SELECT 1 FROM dual"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); } @Test void testDefaultSettings() throws SQLException { try ( // container { OracleContainer oracle = new OracleContainer("gvenzl/oracle-xe:21-slim-faststart") // } ) { runTest(oracle, "xepdb1", "test", "test"); // Match against the last '/' String urlSuffix = oracle.getJdbcUrl().split("(\\/)(?!.*\\/)", 2)[1]; assertThat(urlSuffix).isEqualTo("xepdb1"); } } @Test void testPluggableDatabase() throws SQLException { try (OracleContainer oracle = new OracleContainer(ORACLE_DOCKER_IMAGE_NAME).withDatabaseName("testDB")) { runTest(oracle, "testDB", "test", "test"); } } @Test void testPluggableDatabaseAndCustomUser() throws SQLException { try ( OracleContainer oracle = new OracleContainer("gvenzl/oracle-xe:21-slim-faststart") .withDatabaseName("testDB") .withUsername("testUser") .withPassword("testPassword") ) { runTest(oracle, "testDB", "testUser", "testPassword"); } } @Test void testCustomUser() throws SQLException { try ( OracleContainer oracle = new OracleContainer(ORACLE_DOCKER_IMAGE_NAME) .withUsername("testUser") .withPassword("testPassword") ) { runTest(oracle, "xepdb1", "testUser", "testPassword"); } } @Test void testSID() throws SQLException { try (OracleContainer oracle = new OracleContainer(ORACLE_DOCKER_IMAGE_NAME).usingSid();) { runTest(oracle, "xepdb1", "system", "test"); // Match against the last ':' String urlSuffix = oracle.getJdbcUrl().split("(\\:)(?!.*\\:)", 2)[1]; assertThat(urlSuffix).isEqualTo("xe"); } } @Test void testSIDAndCustomPassword() throws SQLException { try ( OracleContainer oracle = new OracleContainer(ORACLE_DOCKER_IMAGE_NAME) .usingSid() .withPassword("testPassword"); ) { runTest(oracle, "xepdb1", "system", "testPassword"); } } @Test void testErrorPaths() throws SQLException { try (OracleContainer oracle = new OracleContainer(ORACLE_DOCKER_IMAGE_NAME)) { try { oracle.withDatabaseName("XEPDB1"); fail("Should not have been able to set database name to xepdb1."); } catch (IllegalArgumentException e) { //expected } try { oracle.withDatabaseName(""); fail("Should not have been able to set database name to nothing."); } catch (IllegalArgumentException e) { //expected } try { oracle.withUsername("SYSTEM"); fail("Should not have been able to set username to system."); } catch (IllegalArgumentException e) { //expected } try { oracle.withUsername("SYS"); fail("Should not have been able to set username to sys."); } catch (IllegalArgumentException e) { //expected } try { oracle.withUsername(""); fail("Should not have been able to set username to nothing."); } catch (IllegalArgumentException e) { //expected } try { oracle.withPassword(""); fail("Should not have been able to set password to nothing."); } catch (IllegalArgumentException e) { //expected } } } } ================================================ FILE: modules/oracle-xe/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/orientdb/build.gradle ================================================ description = "Testcontainers :: Orientdb" dependencies { api project(":testcontainers") api "com.orientechnologies:orientdb-client:3.2.46" testImplementation 'org.apache.tinkerpop:gremlin-driver:3.8.0' testImplementation "com.orientechnologies:orientdb-gremlin:3.2.46" } ================================================ FILE: modules/orientdb/src/main/java/org/testcontainers/containers/OrientDBContainer.java ================================================ package org.testcontainers.containers; import com.github.dockerjava.api.command.InspectContainerResponse; import com.orientechnologies.orient.core.db.ODatabaseSession; import com.orientechnologies.orient.core.db.ODatabaseType; import com.orientechnologies.orient.core.db.OrientDB; import com.orientechnologies.orient.core.db.OrientDBConfig; import lombok.NonNull; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.ComparableVersion; import org.testcontainers.utility.DockerImageName; import java.io.IOException; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Optional; /** * Testcontainers implementation for OrientDB. *

* Supported image: {@code orientdb} *

* Exposed ports: *

    *
  • Database: 2424
  • *
  • Studio: 2480
  • *
* * @deprecated use {@link org.testcontainers.orientdb.OrientDBContainer} instead. */ @Deprecated public class OrientDBContainer extends GenericContainer { private static final Logger LOGGER = LoggerFactory.getLogger(OrientDBContainer.class); private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("orientdb"); private static final String DEFAULT_TAG = "3.0.24-tp3"; private static final String DEFAULT_USERNAME = "admin"; private static final String DEFAULT_PASSWORD = "admin"; private static final String DEFAULT_SERVER_PASSWORD = "root"; private static final String DEFAULT_DATABASE_NAME = "testcontainers"; private static final int DEFAULT_BINARY_PORT = 2424; private static final int DEFAULT_HTTP_PORT = 2480; private String databaseName; private String serverPassword; private Optional scriptPath = Optional.empty(); private OrientDB orientDB; private ODatabaseSession session; /** * @deprecated use {@link #OrientDBContainer(DockerImageName)} instead */ @Deprecated public OrientDBContainer() { this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } public OrientDBContainer(@NonNull String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public OrientDBContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); serverPassword = DEFAULT_SERVER_PASSWORD; databaseName = DEFAULT_DATABASE_NAME; waitStrategy = Wait.forLogMessage(".*OrientDB Studio available.*", 1); addExposedPorts(DEFAULT_BINARY_PORT, DEFAULT_HTTP_PORT); } @Override protected void configure() { addEnv("ORIENTDB_ROOT_PASSWORD", serverPassword); } public String getDatabaseName() { return databaseName; } public String getTestQueryString() { return "SELECT FROM V"; } public OrientDBContainer withDatabaseName(final String databaseName) { this.databaseName = databaseName; return self(); } public OrientDBContainer withServerPassword(final String serverPassword) { this.serverPassword = serverPassword; return self(); } public OrientDBContainer withScriptPath(String scriptPath) { this.scriptPath = Optional.of(scriptPath); return self(); } @Override protected void containerIsStarted(InspectContainerResponse containerInfo) { orientDB = new OrientDB(getServerUrl(), "root", serverPassword, OrientDBConfig.defaultConfig()); } @Deprecated public OrientDB getOrientDB() { return orientDB; } public String getServerUrl() { return "remote:" + getHost() + ":" + getMappedPort(2424); } public String getDbUrl() { return getServerUrl() + "/" + databaseName; } @Deprecated public ODatabaseSession getSession() { return getSession(DEFAULT_USERNAME, DEFAULT_PASSWORD); } @Deprecated public synchronized ODatabaseSession getSession(String username, String password) { String orientdbVersion = Arrays .stream(this.getContainerInfo().getConfig().getEnv()) .filter(env -> env.startsWith("ORIENTDB_VERSION")) .map(env -> env.split("=")[1]) .findFirst() .orElseThrow(() -> new IllegalStateException("no required env var")); boolean isGreaterThan32 = new ComparableVersion(orientdbVersion).isGreaterThanOrEqualTo("3.2.0"); if (isGreaterThan32) { String script = String.format( "CREATE DATABASE %s plocal users(%s identified by '%s' role admin)", databaseName, username, password ); if (!orientDB.exists(databaseName)) { orientDB.execute(script); } } else { orientDB.createIfNotExists(databaseName, ODatabaseType.PLOCAL); } if (session == null) { session = orientDB.open(databaseName, username, password); scriptPath.ifPresent(path -> loadScript(path, session)); } return session; } @Deprecated private void loadScript(String path, ODatabaseSession session) { try { URL resource = getClass().getClassLoader().getResource(path); if (resource == null) { LOGGER.warn("Could not load classpath init script: {}", scriptPath); throw new RuntimeException( "Could not load classpath init script: " + scriptPath + ". Resource not found." ); } String script = IOUtils.toString(resource, StandardCharsets.UTF_8); session.execute("sql", script); } catch (IOException e) { LOGGER.warn("Could not load classpath init script: {}", scriptPath); throw new RuntimeException("Could not load classpath init script: " + scriptPath, e); } catch (UnsupportedOperationException e) { LOGGER.error("Error while executing init script: {}", scriptPath, e); throw new RuntimeException("Error while executing init script: " + scriptPath, e); } } } ================================================ FILE: modules/orientdb/src/main/java/org/testcontainers/orientdb/OrientDBContainer.java ================================================ package org.testcontainers.orientdb; import com.github.dockerjava.api.command.InspectContainerResponse; import lombok.NonNull; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.DockerImageName; import java.io.IOException; /** * Testcontainers implementation for OrientDB. *

* Supported image: {@code orientdb} *

* Exposed ports: *

    *
  • Database: 2424
  • *
  • Studio: 2480
  • *
*/ public class OrientDBContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("orientdb"); private static final String DEFAULT_USERNAME = "admin"; private static final String DEFAULT_PASSWORD = "admin"; private static final String DEFAULT_SERVER_USER = "root"; private static final String DEFAULT_SERVER_PASSWORD = "root"; private static final String DEFAULT_DATABASE_NAME = "testcontainers"; private static final int DEFAULT_BINARY_PORT = 2424; private static final int DEFAULT_HTTP_PORT = 2480; private String databaseName; private String serverPassword; private Transferable scriptPath; public OrientDBContainer(@NonNull String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public OrientDBContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); this.serverPassword = DEFAULT_SERVER_PASSWORD; this.databaseName = DEFAULT_DATABASE_NAME; waitingFor(Wait.forLogMessage(".*OrientDB Studio available.*", 1)); addExposedPorts(DEFAULT_BINARY_PORT, DEFAULT_HTTP_PORT); } @Override protected void configure() { addEnv("ORIENTDB_ROOT_PASSWORD", serverPassword); } @Override protected void containerIsStarted(InspectContainerResponse containerInfo) { try { String createDb = String.format( "CREATE DATABASE remote:localhost/%s %s %s plocal; CONNECT remote:localhost/%s %s %s; CREATE USER %s IDENTIFIED BY %s ROLE admin;", this.databaseName, DEFAULT_SERVER_USER, this.serverPassword, this.databaseName, DEFAULT_SERVER_USER, this.serverPassword, DEFAULT_USERNAME, DEFAULT_PASSWORD ); execInContainer("/orientdb/bin/console.sh", createDb); if (this.scriptPath != null) { copyFileToContainer(this.scriptPath, "/opt/testcontainers/script.osql"); String loadScript = String.format( "CONNECT remote:localhost/%s %s %s; LOAD SCRIPT /opt/testcontainers/script.osql", this.databaseName, DEFAULT_SERVER_USER, this.serverPassword ); execInContainer("/orientdb/bin/console.sh", loadScript); } } catch (IOException | InterruptedException e) { throw new RuntimeException(e); } } public String getDatabaseName() { return databaseName; } public OrientDBContainer withDatabaseName(final String databaseName) { this.databaseName = databaseName; return self(); } public OrientDBContainer withServerPassword(final String serverPassword) { this.serverPassword = serverPassword; return self(); } public OrientDBContainer withScriptPath(Transferable scriptPath) { this.scriptPath = scriptPath; return self(); } public String getServerUrl() { return "remote:" + getHost() + ":" + getMappedPort(2424); } public String getDbUrl() { return getServerUrl() + "/" + this.databaseName; } public String getServerUser() { return DEFAULT_SERVER_USER; } public String getServerPassword() { return this.serverPassword; } public String getUsername() { return DEFAULT_USERNAME; } public String getPassword() { return DEFAULT_PASSWORD; } } ================================================ FILE: modules/orientdb/src/test/java/org/testcontainers/orientdb/OrientDBContainerTest.java ================================================ package org.testcontainers.orientdb; import com.orientechnologies.orient.core.db.ODatabaseSession; import com.orientechnologies.orient.core.db.OrientDB; import com.orientechnologies.orient.core.db.OrientDBConfig; import org.junit.jupiter.api.Test; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import static org.assertj.core.api.Assertions.assertThat; class OrientDBContainerTest { private static final DockerImageName ORIENTDB_IMAGE = DockerImageName.parse("orientdb:3.2.0-tp3"); @Test void shouldInitializeWithCommands() { try ( // container { OrientDBContainer orientdb = new OrientDBContainer("orientdb:3.2.0-tp3") // } ) { orientdb.start(); OrientDB orientDB = new OrientDB( orientdb.getServerUrl(), orientdb.getServerUser(), orientdb.getServerPassword(), OrientDBConfig.defaultConfig() ); ODatabaseSession session = orientDB.open( orientdb.getDatabaseName(), orientdb.getUsername(), orientdb.getPassword() ); session.command("CREATE CLASS Person EXTENDS V"); session.command("INSERT INTO Person set name='john'"); session.command("INSERT INTO Person set name='jane'"); assertThat(session.query("SELECT FROM Person").stream()).hasSize(2); } } @Test void shouldQueryWithGremlin() { try ( OrientDBContainer orientdb = new OrientDBContainer(ORIENTDB_IMAGE) .withCopyFileToContainer( MountableFile.forClasspathResource("orientdb-server-config.xml"), "/orientdb/config/orientdb-server-config.xml" ) ) { orientdb.start(); OrientDB orientDB = new OrientDB( orientdb.getServerUrl(), orientdb.getServerUser(), orientdb.getServerPassword(), OrientDBConfig.defaultConfig() ); ODatabaseSession session = orientDB.open( orientdb.getDatabaseName(), orientdb.getUsername(), orientdb.getPassword() ); session.command("CREATE CLASS Person EXTENDS V"); session.command("INSERT INTO Person set name='john'"); session.command("INSERT INTO Person set name='jane'"); assertThat(session.execute("gremlin", "g.V().hasLabel('Person')").stream()).hasSize(2); } } @Test void shouldInitializeDatabaseFromScript() { try ( OrientDBContainer orientdb = new OrientDBContainer(ORIENTDB_IMAGE) .withScriptPath(MountableFile.forClasspathResource("initscript.osql")) .withDatabaseName("persons") ) { orientdb.start(); assertThat(orientdb.getDbUrl()) .isEqualTo("remote:" + orientdb.getHost() + ":" + orientdb.getMappedPort(2424) + "/persons"); OrientDB orientDB = new OrientDB( orientdb.getServerUrl(), orientdb.getServerUser(), orientdb.getServerPassword(), OrientDBConfig.defaultConfig() ); ODatabaseSession session = orientDB.open( orientdb.getDatabaseName(), orientdb.getUsername(), orientdb.getPassword() ); assertThat(session.query("SELECT FROM Person").stream()).hasSize(4); } } } ================================================ FILE: modules/orientdb/src/test/resources/initscript.osql ================================================ CREATE CLASS Person EXTENDS V; INSERT INTO Person set name="john"; INSERT INTO Person set name="paul"; INSERT INTO Person set name="luke"; INSERT INTO Person set name="albert"; ================================================ FILE: modules/orientdb/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/orientdb/src/test/resources/orientdb-server-config.xml ================================================ ================================================ FILE: modules/pinecone/build.gradle ================================================ description = "Testcontainers :: Pinecone" dependencies { api project(':testcontainers') testImplementation 'io.pinecone:pinecone-client:3.1.0' } ================================================ FILE: modules/pinecone/src/main/java/org/testcontainers/pinecone/PineconeLocalContainer.java ================================================ package org.testcontainers.pinecone; import org.testcontainers.containers.GenericContainer; import org.testcontainers.utility.DockerImageName; /** * Testcontainers implementation for Pinecone. *

* Exposed port: 5080 */ public class PineconeLocalContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse( "ghcr.io/pinecone-io/pinecone-local" ); private static final int PORT = 5080; public PineconeLocalContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public PineconeLocalContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); withEnv("PORT", String.valueOf(5080)); withExposedPorts(5080); } public String getEndpoint() { return "http://" + getHost() + ":" + getMappedPort(PORT); } } ================================================ FILE: modules/pinecone/src/test/java/org/testcontainers/pinecone/PineconeLocalContainerTest.java ================================================ package org.testcontainers.pinecone; import io.pinecone.clients.Pinecone; import org.junit.jupiter.api.Test; import org.openapitools.db_control.client.model.DeletionProtection; import org.openapitools.db_control.client.model.IndexModel; import static org.assertj.core.api.Assertions.assertThat; class PineconeLocalContainerTest { @Test void testSimple() { try ( // container { PineconeLocalContainer container = new PineconeLocalContainer("ghcr.io/pinecone-io/pinecone-local:v0.7.0") // } ) { container.start(); // client { Pinecone pinecone = new Pinecone.Builder("pclocal") .withHost(container.getEndpoint()) .withTlsEnabled(false) .build(); // } String indexName = "example-index"; pinecone.createServerlessIndex(indexName, "cosine", 2, "aws", "us-east-1", DeletionProtection.DISABLED); IndexModel indexModel = pinecone.describeIndex(indexName); assertThat(indexModel.getDeletionProtection()).isEqualTo(DeletionProtection.DISABLED); } } } ================================================ FILE: modules/pinecone/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/postgresql/build.gradle ================================================ description = "Testcontainers :: JDBC :: PostgreSQL" dependencies { api project(':testcontainers-jdbc') compileOnly project(':testcontainers-r2dbc') compileOnly 'io.r2dbc:r2dbc-postgresql:0.8.13.RELEASE' testImplementation project(':testcontainers-jdbc-test') testRuntimeOnly 'org.postgresql:postgresql:42.7.8' testImplementation testFixtures(project(':testcontainers-r2dbc')) testRuntimeOnly 'io.r2dbc:r2dbc-postgresql:0.8.13.RELEASE' compileOnly 'org.jetbrains:annotations:26.0.2-1' } ================================================ FILE: modules/postgresql/src/main/java/org/testcontainers/containers/PgVectorContainerProvider.java ================================================ package org.testcontainers.containers; import org.testcontainers.jdbc.ConnectionUrl; import org.testcontainers.utility.DockerImageName; /** * Factory for PgVector containers. * * @see https://github.com/pgvector/pgvector */ public class PgVectorContainerProvider extends JdbcDatabaseContainerProvider { private static final String NAME = "pgvector"; private static final String DEFAULT_TAG = "pg16"; private static final DockerImageName DEFAULT_IMAGE = DockerImageName.parse("pgvector/pgvector"); public static final String USER_PARAM = "user"; public static final String PASSWORD_PARAM = "password"; @Override public boolean supports(String databaseType) { return databaseType.equals(NAME); } @Override public JdbcDatabaseContainer newInstance() { return newInstance(DEFAULT_TAG); } @Override public JdbcDatabaseContainer newInstance(String tag) { return new PostgreSQLContainer(DEFAULT_IMAGE.withTag(tag)); } @Override public JdbcDatabaseContainer newInstance(ConnectionUrl connectionUrl) { return newInstanceFromConnectionUrl(connectionUrl, USER_PARAM, PASSWORD_PARAM); } } ================================================ FILE: modules/postgresql/src/main/java/org/testcontainers/containers/PostgisContainerProvider.java ================================================ package org.testcontainers.containers; import org.testcontainers.jdbc.ConnectionUrl; import org.testcontainers.utility.DockerImageName; /** * Factory for PostGIS containers, which are a special flavour of PostgreSQL. */ public class PostgisContainerProvider extends JdbcDatabaseContainerProvider { private static final String NAME = "postgis"; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName .parse("postgis/postgis") .asCompatibleSubstituteFor("postgres"); private static final String DEFAULT_TAG = "12-3.0"; public static final String USER_PARAM = "user"; public static final String PASSWORD_PARAM = "password"; @Override public boolean supports(String databaseType) { return databaseType.equals(NAME); } @Override public JdbcDatabaseContainer newInstance() { return newInstance(DEFAULT_TAG); } @Override public JdbcDatabaseContainer newInstance(String tag) { return new PostgreSQLContainer<>(DEFAULT_IMAGE_NAME.withTag(tag)); } @Override public JdbcDatabaseContainer newInstance(ConnectionUrl connectionUrl) { return newInstanceFromConnectionUrl(connectionUrl, USER_PARAM, PASSWORD_PARAM); } } ================================================ FILE: modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLContainer.java ================================================ package org.testcontainers.containers; import org.jetbrains.annotations.NotNull; import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; import org.testcontainers.utility.DockerImageName; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.Set; /** * Testcontainers implementation for PostgreSQL. *

* Supported images: {@code postgres}, {@code pgvector/pgvector} *

* Exposed ports: 5432 * * @deprecated use {@link org.testcontainers.postgresql.PostgreSQLContainer} instead. */ @Deprecated public class PostgreSQLContainer> extends JdbcDatabaseContainer { public static final String NAME = "postgresql"; public static final String IMAGE = "postgres"; public static final String DEFAULT_TAG = "9.6.12"; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("postgres"); private static final DockerImageName PGVECTOR_IMAGE_NAME = DockerImageName.parse("pgvector/pgvector"); public static final Integer POSTGRESQL_PORT = 5432; static final String DEFAULT_USER = "test"; static final String DEFAULT_PASSWORD = "test"; private String databaseName = "test"; private String username = "test"; private String password = "test"; private static final String FSYNC_OFF_OPTION = "fsync=off"; /** * @deprecated use {@link #PostgreSQLContainer(DockerImageName)} or {@link #PostgreSQLContainer(String)} instead */ @Deprecated public PostgreSQLContainer() { this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } public PostgreSQLContainer(final String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public PostgreSQLContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, PGVECTOR_IMAGE_NAME); this.waitStrategy = new LogMessageWaitStrategy() .withRegEx(".*database system is ready to accept connections.*\\s") .withTimes(2) .withStartupTimeout(Duration.of(60, ChronoUnit.SECONDS)); this.setCommand("postgres", "-c", FSYNC_OFF_OPTION); addExposedPort(POSTGRESQL_PORT); } /** * @return the ports on which to check if the container is ready * @deprecated use {@link #getLivenessCheckPortNumbers()} instead */ @NotNull @Override @Deprecated protected Set getLivenessCheckPorts() { return super.getLivenessCheckPorts(); } @Override protected void configure() { // Disable Postgres driver use of java.util.logging to reduce noise at startup time withUrlParam("loggerLevel", "OFF"); addEnv("POSTGRES_DB", databaseName); addEnv("POSTGRES_USER", username); addEnv("POSTGRES_PASSWORD", password); } @Override public String getDriverClassName() { return "org.postgresql.Driver"; } @Override public String getJdbcUrl() { String additionalUrlParams = constructUrlParameters("?", "&"); return ( "jdbc:postgresql://" + getHost() + ":" + getMappedPort(POSTGRESQL_PORT) + "/" + databaseName + additionalUrlParams ); } @Override public String getDatabaseName() { return databaseName; } @Override public String getUsername() { return username; } @Override public String getPassword() { return password; } @Override public String getTestQueryString() { return "SELECT 1"; } @Override public SELF withDatabaseName(final String databaseName) { this.databaseName = databaseName; return self(); } @Override public SELF withUsername(final String username) { this.username = username; return self(); } @Override public SELF withPassword(final String password) { this.password = password; return self(); } @Override protected void waitUntilContainerStarted() { getWaitStrategy().waitUntilReady(this); } } ================================================ FILE: modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLContainerProvider.java ================================================ package org.testcontainers.containers; import org.testcontainers.jdbc.ConnectionUrl; import org.testcontainers.utility.DockerImageName; /** * Factory for PostgreSQL containers. */ public class PostgreSQLContainerProvider extends JdbcDatabaseContainerProvider { public static final String USER_PARAM = "user"; public static final String PASSWORD_PARAM = "password"; @Override public boolean supports(String databaseType) { return databaseType.equals(PostgreSQLContainer.NAME); } @Override public JdbcDatabaseContainer newInstance() { return newInstance(PostgreSQLContainer.DEFAULT_TAG); } @Override public JdbcDatabaseContainer newInstance(String tag) { return new PostgreSQLContainer(DockerImageName.parse(PostgreSQLContainer.IMAGE).withTag(tag)); } @Override public JdbcDatabaseContainer newInstance(ConnectionUrl connectionUrl) { return newInstanceFromConnectionUrl(connectionUrl, USER_PARAM, PASSWORD_PARAM); } } ================================================ FILE: modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainer.java ================================================ package org.testcontainers.containers; import io.r2dbc.spi.ConnectionFactoryOptions; import lombok.RequiredArgsConstructor; import lombok.experimental.Delegate; import org.testcontainers.lifecycle.Startable; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; @RequiredArgsConstructor public final class PostgreSQLR2DBCDatabaseContainer implements R2DBCDatabaseContainer { @Delegate(types = Startable.class) private final PostgreSQLContainer container; public static ConnectionFactoryOptions getOptions(PostgreSQLContainer container) { ConnectionFactoryOptions options = ConnectionFactoryOptions .builder() .option(ConnectionFactoryOptions.DRIVER, PostgreSQLR2DBCDatabaseContainerProvider.DRIVER) .build(); return new PostgreSQLR2DBCDatabaseContainer(container).configure(options); } @Override public ConnectionFactoryOptions configure(ConnectionFactoryOptions options) { return options .mutate() .option(ConnectionFactoryOptions.HOST, container.getHost()) .option(ConnectionFactoryOptions.PORT, container.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT)) .option(ConnectionFactoryOptions.DATABASE, container.getDatabaseName()) .option(ConnectionFactoryOptions.USER, container.getUsername()) .option(ConnectionFactoryOptions.PASSWORD, container.getPassword()) .build(); } } ================================================ FILE: modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerProvider.java ================================================ package org.testcontainers.containers; import io.r2dbc.postgresql.PostgresqlConnectionFactoryProvider; import io.r2dbc.spi.ConnectionFactoryMetadata; import io.r2dbc.spi.ConnectionFactoryOptions; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; import org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider; import javax.annotation.Nullable; public final class PostgreSQLR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider { static final String DRIVER = PostgresqlConnectionFactoryProvider.POSTGRESQL_DRIVER; @Override public boolean supports(ConnectionFactoryOptions options) { return DRIVER.equals(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)); } @Override public R2DBCDatabaseContainer createContainer(ConnectionFactoryOptions options) { String image = PostgreSQLContainer.IMAGE + ":" + options.getRequiredValue(IMAGE_TAG_OPTION); PostgreSQLContainer container = new PostgreSQLContainer<>(image) .withDatabaseName((String) options.getRequiredValue(ConnectionFactoryOptions.DATABASE)); if (Boolean.TRUE.equals(options.getValue(REUSABLE_OPTION))) { container.withReuse(true); } return new PostgreSQLR2DBCDatabaseContainer(container); } @Nullable @Override public ConnectionFactoryMetadata getMetadata(ConnectionFactoryOptions options) { ConnectionFactoryOptions.Builder builder = options.mutate(); if (!options.hasOption(ConnectionFactoryOptions.USER)) { builder.option(ConnectionFactoryOptions.USER, PostgreSQLContainer.DEFAULT_USER); } if (!options.hasOption(ConnectionFactoryOptions.PASSWORD)) { builder.option(ConnectionFactoryOptions.PASSWORD, PostgreSQLContainer.DEFAULT_PASSWORD); } return R2DBCDatabaseContainerProvider.super.getMetadata(builder.build()); } } ================================================ FILE: modules/postgresql/src/main/java/org/testcontainers/containers/TimescaleDBContainerProvider.java ================================================ package org.testcontainers.containers; import org.testcontainers.jdbc.ConnectionUrl; import org.testcontainers.utility.DockerImageName; /** * Factory for TimescaleDB containers, which are a special flavour of PostgreSQL. * * @see https://docs.timescale.com/latest/introduction */ public class TimescaleDBContainerProvider extends JdbcDatabaseContainerProvider { private static final String NAME = "timescaledb"; private static final String DEFAULT_TAG = "2.1.0-pg11"; private static final DockerImageName DEFAULT_IMAGE = DockerImageName .parse("timescale/timescaledb") .asCompatibleSubstituteFor("postgres"); public static final String USER_PARAM = "user"; public static final String PASSWORD_PARAM = "password"; @Override public boolean supports(String databaseType) { return databaseType.equals(NAME); } @Override public JdbcDatabaseContainer newInstance() { return newInstance(DEFAULT_TAG); } @Override public JdbcDatabaseContainer newInstance(String tag) { return new PostgreSQLContainer(DEFAULT_IMAGE.withTag(tag)); } @Override public JdbcDatabaseContainer newInstance(ConnectionUrl connectionUrl) { return newInstanceFromConnectionUrl(connectionUrl, USER_PARAM, PASSWORD_PARAM); } } ================================================ FILE: modules/postgresql/src/main/java/org/testcontainers/postgresql/PostgreSQLContainer.java ================================================ package org.testcontainers.postgresql; import org.jetbrains.annotations.NotNull; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.Set; /** * Testcontainers implementation for PostgreSQL. *

* Supported images: {@code postgres}, {@code pgvector/pgvector} *

* Exposed ports: 5432 */ public class PostgreSQLContainer extends JdbcDatabaseContainer { public static final String NAME = "postgresql"; public static final String IMAGE = "postgres"; public static final String DEFAULT_TAG = "9.6.12"; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("postgres"); private static final DockerImageName PGVECTOR_IMAGE_NAME = DockerImageName.parse("pgvector/pgvector"); public static final Integer POSTGRESQL_PORT = 5432; static final String DEFAULT_USER = "test"; static final String DEFAULT_PASSWORD = "test"; private String databaseName = "test"; private String username = "test"; private String password = "test"; private static final String FSYNC_OFF_OPTION = "fsync=off"; public PostgreSQLContainer(final String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public PostgreSQLContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, PGVECTOR_IMAGE_NAME); waitingFor( Wait .forLogMessage(".*database system is ready to accept connections.*\\s", 2) .withStartupTimeout(Duration.of(60, ChronoUnit.SECONDS)) ); setCommand("postgres", "-c", FSYNC_OFF_OPTION); addExposedPort(POSTGRESQL_PORT); } /** * @return the ports on which to check if the container is ready * @deprecated use {@link #getLivenessCheckPortNumbers()} instead */ @NotNull @Override @Deprecated protected Set getLivenessCheckPorts() { return super.getLivenessCheckPorts(); } @Override protected void configure() { // Disable Postgres driver use of java.util.logging to reduce noise at startup time withUrlParam("loggerLevel", "OFF"); addEnv("POSTGRES_DB", databaseName); addEnv("POSTGRES_USER", username); addEnv("POSTGRES_PASSWORD", password); } @Override public String getDriverClassName() { return "org.postgresql.Driver"; } @Override public String getJdbcUrl() { String additionalUrlParams = constructUrlParameters("?", "&"); return ( "jdbc:postgresql://" + getHost() + ":" + getMappedPort(POSTGRESQL_PORT) + "/" + databaseName + additionalUrlParams ); } @Override public String getDatabaseName() { return databaseName; } @Override public String getUsername() { return username; } @Override public String getPassword() { return password; } @Override public String getTestQueryString() { return "SELECT 1"; } @Override public PostgreSQLContainer withDatabaseName(final String databaseName) { this.databaseName = databaseName; return self(); } @Override public PostgreSQLContainer withUsername(final String username) { this.username = username; return self(); } @Override public PostgreSQLContainer withPassword(final String password) { this.password = password; return self(); } @Override protected void waitUntilContainerStarted() { getWaitStrategy().waitUntilReady(this); } } ================================================ FILE: modules/postgresql/src/main/java/org/testcontainers/postgresql/PostgreSQLR2DBCDatabaseContainer.java ================================================ package org.testcontainers.postgresql; import io.r2dbc.postgresql.PostgresqlConnectionFactoryProvider; import io.r2dbc.spi.ConnectionFactoryOptions; import org.testcontainers.lifecycle.Startable; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; import java.util.Set; public final class PostgreSQLR2DBCDatabaseContainer implements R2DBCDatabaseContainer { private final PostgreSQLContainer container; public PostgreSQLR2DBCDatabaseContainer(PostgreSQLContainer container) { this.container = container; } public static ConnectionFactoryOptions getOptions(PostgreSQLContainer container) { ConnectionFactoryOptions options = ConnectionFactoryOptions .builder() .option(ConnectionFactoryOptions.DRIVER, PostgresqlConnectionFactoryProvider.POSTGRESQL_DRIVER) .build(); return new PostgreSQLR2DBCDatabaseContainer(container).configure(options); } @Override public ConnectionFactoryOptions configure(ConnectionFactoryOptions options) { return options .mutate() .option(ConnectionFactoryOptions.HOST, container.getHost()) .option(ConnectionFactoryOptions.PORT, container.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT)) .option(ConnectionFactoryOptions.DATABASE, container.getDatabaseName()) .option(ConnectionFactoryOptions.USER, container.getUsername()) .option(ConnectionFactoryOptions.PASSWORD, container.getPassword()) .build(); } @Override public Set getDependencies() { return this.container.getDependencies(); } @Override public void start() { this.container.start(); } @Override public void stop() { this.container.stop(); } @Override public void close() { this.container.close(); } } ================================================ FILE: modules/postgresql/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider ================================================ org.testcontainers.containers.PostgreSQLContainerProvider org.testcontainers.containers.PostgisContainerProvider org.testcontainers.containers.TimescaleDBContainerProvider org.testcontainers.containers.PgVectorContainerProvider ================================================ FILE: modules/postgresql/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider ================================================ org.testcontainers.containers.PostgreSQLR2DBCDatabaseContainerProvider ================================================ FILE: modules/postgresql/src/test/java/org/testcontainers/PostgreSQLTestImages.java ================================================ package org.testcontainers; import org.testcontainers.utility.DockerImageName; public interface PostgreSQLTestImages { DockerImageName POSTGRES_TEST_IMAGE = DockerImageName.parse("postgres:9.6.12"); } ================================================ FILE: modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLConnectionURLTest.java ================================================ package org.testcontainers.containers; import org.junit.jupiter.api.Test; import org.testcontainers.PostgreSQLTestImages; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; class PostgreSQLConnectionURLTest { @Test void shouldCorrectlyAppendQueryString() { PostgreSQLContainer postgres = new FixedJdbcUrlPostgreSQLContainer(); String connectionUrl = postgres.constructUrlForConnection("?stringtype=unspecified&stringtype=unspecified"); String queryString = connectionUrl.substring(connectionUrl.indexOf('?')); assertThat(queryString) .as("Query String contains expected params") .contains("?stringtype=unspecified&stringtype=unspecified"); assertThat(queryString.indexOf('?')).as("Query String starts with '?'").isZero(); assertThat(queryString.substring(1)).as("Query String does not contain extra '?'").doesNotContain("?"); } @Test void shouldCorrectlyAppendQueryStringWhenNoBaseParams() { PostgreSQLContainer postgres = new NoParamsUrlPostgreSQLContainer(); String connectionUrl = postgres.constructUrlForConnection("?stringtype=unspecified&stringtype=unspecified"); String queryString = connectionUrl.substring(connectionUrl.indexOf('?')); assertThat(queryString) .as("Query String contains expected params") .contains("?stringtype=unspecified&stringtype=unspecified"); assertThat(queryString.indexOf('?')).as("Query String starts with '?'").isZero(); assertThat(queryString.substring(1)).as("Query String does not contain extra '?'").doesNotContain("?"); } @Test void shouldReturnOriginalURLWhenEmptyQueryString() { PostgreSQLContainer postgres = new FixedJdbcUrlPostgreSQLContainer(); String connectionUrl = postgres.constructUrlForConnection(""); assertThat(postgres.getJdbcUrl()).as("Query String remains unchanged").isEqualTo(connectionUrl); } @Test void shouldRejectInvalidQueryString() { assertThat( catchThrowable(() -> { new NoParamsUrlPostgreSQLContainer().constructUrlForConnection("stringtype=unspecified"); }) ) .as("Fails when invalid query string provided") .isInstanceOf(IllegalArgumentException.class); } static class FixedJdbcUrlPostgreSQLContainer extends PostgreSQLContainer { public FixedJdbcUrlPostgreSQLContainer() { super(PostgreSQLTestImages.POSTGRES_TEST_IMAGE); } @Override public String getHost() { return "localhost"; } @Override public Integer getMappedPort(int originalPort) { return 34532; } } static class NoParamsUrlPostgreSQLContainer extends PostgreSQLContainer { public NoParamsUrlPostgreSQLContainer() { super(PostgreSQLTestImages.POSTGRES_TEST_IMAGE); } @Override public String getJdbcUrl() { return "jdbc:postgresql://host:port/database"; } } } ================================================ FILE: modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerTest.java ================================================ package org.testcontainers.containers; import io.r2dbc.spi.ConnectionFactoryOptions; import org.testcontainers.PostgreSQLTestImages; import org.testcontainers.r2dbc.AbstractR2DBCDatabaseContainerTest; public class PostgreSQLR2DBCDatabaseContainerTest extends AbstractR2DBCDatabaseContainerTest> { @Override protected PostgreSQLContainer createContainer() { return new PostgreSQLContainer<>(PostgreSQLTestImages.POSTGRES_TEST_IMAGE); } @Override protected ConnectionFactoryOptions getOptions(PostgreSQLContainer container) { // spotless:off // get_options { ConnectionFactoryOptions options = PostgreSQLR2DBCDatabaseContainer.getOptions( container ); // } // spotless:on return options; } protected String createR2DBCUrl() { return "r2dbc:tc:postgresql:///db?TC_IMAGE_TAG=10-alpine"; } } ================================================ FILE: modules/postgresql/src/test/java/org/testcontainers/containers/TimescaleDBContainerTest.java ================================================ package org.testcontainers.containers; import org.junit.jupiter.api.Test; import org.testcontainers.db.AbstractContainerDatabaseTest; import java.sql.ResultSet; import java.sql.SQLException; import static org.assertj.core.api.Assertions.assertThat; class TimescaleDBContainerTest extends AbstractContainerDatabaseTest { @Test void testSimple() throws SQLException { try (JdbcDatabaseContainer postgres = new TimescaleDBContainerProvider().newInstance()) { postgres.start(); ResultSet resultSet = performQuery(postgres, "SELECT 1"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); } } @Test void testCommandOverride() throws SQLException { try ( GenericContainer postgres = new TimescaleDBContainerProvider() .newInstance() .withCommand("postgres -c max_connections=42") ) { postgres.start(); ResultSet resultSet = performQuery( (JdbcDatabaseContainer) postgres, "SELECT current_setting('max_connections')" ); String result = resultSet.getString(1); assertThat(result).as("max_connections should be overridden").isEqualTo("42"); } } @Test void testUnsetCommand() throws SQLException { try ( GenericContainer postgres = new TimescaleDBContainerProvider() .newInstance() .withCommand("postgres -c max_connections=42") .withCommand() ) { postgres.start(); ResultSet resultSet = performQuery( (JdbcDatabaseContainer) postgres, "SELECT current_setting('max_connections')" ); String result = resultSet.getString(1); assertThat(result).as("max_connections should not be overridden").isNotEqualTo("42"); } } @Test void testExplicitInitScript() throws SQLException { try ( JdbcDatabaseContainer postgres = new TimescaleDBContainerProvider() .newInstance() .withInitScript("somepath/init_timescaledb.sql") ) { postgres.start(); ResultSet resultSet = performQuery(postgres, "SELECT foo FROM bar"); String firstColumnValue = resultSet.getString(1); assertThat(firstColumnValue).as("Value from init script should equal real value").isEqualTo("hello world"); } } } ================================================ FILE: modules/postgresql/src/test/java/org/testcontainers/jdbc/DatabaseDriverShutdownTest.java ================================================ package org.testcontainers.jdbc; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.testcontainers.containers.JdbcDatabaseContainer; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import static org.assertj.core.api.Assertions.assertThat; /** * This test belongs in the jdbc module, as it is focused on testing the behaviour of {@link org.testcontainers.containers.JdbcDatabaseContainer}. * However, the need to use the {@link org.testcontainers.containers.PostgreSQLContainerProvider} (due to the jdbc:tc:postgresql) URL forces it to live here in * the mysql module, to avoid circular dependencies. * TODO: Move to the jdbc module and either (a) implement a barebones {@link org.testcontainers.containers.JdbcDatabaseContainerProvider} for testing, or (b) refactor into a unit test. */ class DatabaseDriverShutdownTest { @BeforeAll public static void testCleanup() { ContainerDatabaseDriver.killContainers(); } @Test void shouldStopContainerWhenAllConnectionsClosed() throws SQLException { final String jdbcUrl = "jdbc:tc:postgresql:9.6.8://hostname/databasename"; getConnectionAndClose(jdbcUrl); JdbcDatabaseContainer container = ContainerDatabaseDriver.getContainer(jdbcUrl); assertThat(container).as("Database container instance is null as expected").isNull(); } @Test void shouldNotStopDaemonContainerWhenAllConnectionsClosed() throws SQLException { final String jdbcUrl = "jdbc:tc:postgresql:9.6.8://hostname/databasename?TC_DAEMON=true"; getConnectionAndClose(jdbcUrl); JdbcDatabaseContainer container = ContainerDatabaseDriver.getContainer(jdbcUrl); assertThat(container).as("Database container instance is not null as expected").isNotNull(); assertThat(container.isRunning()).as("Database container is running as expected").isTrue(); } private void getConnectionAndClose(String jdbcUrl) throws SQLException { try (Connection connection = DriverManager.getConnection(jdbcUrl)) { assertThat(connection).as("Obtained connection as expected").isNotNull(); } } } ================================================ FILE: modules/postgresql/src/test/java/org/testcontainers/jdbc/DatabaseDriverTmpfsTest.java ================================================ package org.testcontainers.jdbc; import org.junit.jupiter.api.Test; import org.testcontainers.containers.Container; import org.testcontainers.containers.JdbcDatabaseContainer; import java.sql.Connection; import java.sql.DriverManager; import static org.assertj.core.api.Assertions.assertThat; /** * This test belongs in the jdbc module, as it is focused on testing the behaviour of {@link org.testcontainers.containers.JdbcDatabaseContainer}. * However, the need to use the {@link org.testcontainers.containers.PostgreSQLContainerProvider} (due to the jdbc:tc:postgresql) URL forces it to live here in * the mysql module, to avoid circular dependencies. * TODO: Move to the jdbc module and either (a) implement a barebones {@link org.testcontainers.containers.JdbcDatabaseContainerProvider} for testing, or (b) refactor into a unit test. */ class DatabaseDriverTmpfsTest { @Test void testDatabaseHasTmpFsViaConnectionString() throws Exception { final String jdbcUrl = "jdbc:tc:postgresql:9.6.8://hostname/databasename?TC_TMPFS=/testtmpfs:rw"; try (Connection ignored = DriverManager.getConnection(jdbcUrl)) { JdbcDatabaseContainer container = ContainerDatabaseDriver.getContainer(jdbcUrl); // check file doesn't exist String path = "/testtmpfs/test.file"; Container.ExecResult execResult = container.execInContainer("ls", path); assertThat(execResult.getExitCode()) .as("tmpfs inside container doesn't have file that doesn't exist") .isNotZero(); // touch && check file does exist container.execInContainer("touch", path); execResult = container.execInContainer("ls", path); assertThat(execResult.getExitCode()).as("tmpfs inside container has file that does exist").isZero(); } } } ================================================ FILE: modules/postgresql/src/test/java/org/testcontainers/jdbc/pgvector/PgVectorJDBCDriverTest.java ================================================ package org.testcontainers.jdbc.pgvector; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; class PgVectorJDBCDriverTest extends AbstractJDBCDriverTest { public static Iterable data() { return Arrays.asList( new Object[][] { { "jdbc:tc:pgvector://hostname/databasename?user=someuser&password=somepwd", EnumSet.of(Options.JDBCParams), }, { "jdbc:tc:pgvector:pg14://hostname/databasename?user=someuser&password=somepwd", EnumSet.of(Options.JDBCParams), }, } ); } } ================================================ FILE: modules/postgresql/src/test/java/org/testcontainers/jdbc/postgis/PostgisJDBCDriverTest.java ================================================ package org.testcontainers.jdbc.postgis; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; class PostgisJDBCDriverTest extends AbstractJDBCDriverTest { public static Iterable data() { return Arrays.asList( new Object[][] { { "jdbc:tc:postgis://hostname/databasename?user=someuser&password=somepwd", EnumSet.of(Options.JDBCParams), }, { "jdbc:tc:postgis:9.6-2.5://hostname/databasename?user=someuser&password=somepwd", EnumSet.of(Options.JDBCParams), }, } ); } } ================================================ FILE: modules/postgresql/src/test/java/org/testcontainers/jdbc/postgresql/PostgreSQLJDBCDriverTest.java ================================================ package org.testcontainers.jdbc.postgresql; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; public class PostgreSQLJDBCDriverTest extends AbstractJDBCDriverTest { public static Iterable data() { return Arrays.asList( new Object[][] { { "jdbc:tc:postgresql:9.6.8://hostname/databasename?user=someuser&password=somepwd", EnumSet.of(Options.JDBCParams), }, } ); } } ================================================ FILE: modules/postgresql/src/test/java/org/testcontainers/jdbc/timescaledb/TimescaleDBJDBCDriverTest.java ================================================ package org.testcontainers.jdbc.timescaledb; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; public class TimescaleDBJDBCDriverTest extends AbstractJDBCDriverTest { public static Iterable data() { return Arrays.asList( new Object[][] { { "jdbc:tc:timescaledb://hostname/databasename?user=someuser&password=somepwd", EnumSet.of(Options.JDBCParams), }, { "jdbc:tc:timescaledb:2.1.0-pg13://hostname/databasename?user=someuser&password=somepwd", EnumSet.of(Options.JDBCParams), }, } ); } } ================================================ FILE: modules/postgresql/src/test/java/org/testcontainers/postgresql/CompatibleImageTest.java ================================================ package org.testcontainers.postgresql; import org.junit.jupiter.api.Test; import org.testcontainers.db.AbstractContainerDatabaseTest; import org.testcontainers.utility.DockerImageName; import java.sql.ResultSet; import java.sql.SQLException; import static org.assertj.core.api.Assertions.assertThat; class CompatibleImageTest extends AbstractContainerDatabaseTest { @Test void pgvector() throws SQLException { try ( // pgvectorContainer { PostgreSQLContainer pgvector = new PostgreSQLContainer("pgvector/pgvector:pg16") // } ) { pgvector.start(); ResultSet resultSet = performQuery(pgvector, "SELECT 1"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); } } @Test void postgis() throws SQLException { try ( // postgisContainer { PostgreSQLContainer postgis = new PostgreSQLContainer( DockerImageName.parse("postgis/postgis:16-3.4-alpine").asCompatibleSubstituteFor("postgres") ) // } ) { postgis.start(); ResultSet resultSet = performQuery(postgis, "SELECT 1"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); } } @Test void timescaledb() throws SQLException { try ( // timescaledbContainer { PostgreSQLContainer timescaledb = new PostgreSQLContainer( DockerImageName.parse("timescale/timescaledb:2.14.2-pg16").asCompatibleSubstituteFor("postgres") ) // } ) { timescaledb.start(); ResultSet resultSet = performQuery(timescaledb, "SELECT 1"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); } } } ================================================ FILE: modules/postgresql/src/test/java/org/testcontainers/postgresql/PostgreSQLContainerTest.java ================================================ package org.testcontainers.postgresql; import org.junit.jupiter.api.Test; import org.testcontainers.PostgreSQLTestImages; import org.testcontainers.db.AbstractContainerDatabaseTest; import java.sql.ResultSet; import java.sql.SQLException; import java.util.logging.Level; import java.util.logging.LogManager; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNoException; class PostgreSQLContainerTest extends AbstractContainerDatabaseTest { static { // Postgres JDBC driver uses JUL; disable it to avoid annoying, irrelevant, stderr logs during connection testing LogManager.getLogManager().getLogger("").setLevel(Level.OFF); } @Test void testSimple() throws SQLException { try ( // container { PostgreSQLContainer postgres = new PostgreSQLContainer("postgres:9.6.12") // } ) { postgres.start(); ResultSet resultSet = performQuery(postgres, "SELECT 1"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); assertHasCorrectExposedAndLivenessCheckPorts(postgres); } } @Test void testCommandOverride() throws SQLException { try ( PostgreSQLContainer postgres = new PostgreSQLContainer(PostgreSQLTestImages.POSTGRES_TEST_IMAGE) .withCommand("postgres -c max_connections=42") ) { postgres.start(); ResultSet resultSet = performQuery(postgres, "SELECT current_setting('max_connections')"); String result = resultSet.getString(1); assertThat(result).as("max_connections should be overridden").isEqualTo("42"); } } @Test void testUnsetCommand() throws SQLException { try ( PostgreSQLContainer postgres = new PostgreSQLContainer(PostgreSQLTestImages.POSTGRES_TEST_IMAGE) .withCommand("postgres -c max_connections=42") .withCommand() ) { postgres.start(); ResultSet resultSet = performQuery(postgres, "SELECT current_setting('max_connections')"); String result = resultSet.getString(1); assertThat(result).as("max_connections should not be overridden").isNotEqualTo("42"); } } @Test void testMissingInitScript() { try ( PostgreSQLContainer postgres = new PostgreSQLContainer(PostgreSQLTestImages.POSTGRES_TEST_IMAGE) .withInitScript(null) ) { assertThatNoException().isThrownBy(postgres::start); } } @Test void testExplicitInitScript() throws SQLException { try ( PostgreSQLContainer postgres = new PostgreSQLContainer(PostgreSQLTestImages.POSTGRES_TEST_IMAGE) .withInitScript("somepath/init_postgresql.sql") ) { postgres.start(); ResultSet resultSet = performQuery(postgres, "SELECT foo FROM bar"); String firstColumnValue = resultSet.getString(1); assertThat(firstColumnValue).as("Value from init script should equal real value").isEqualTo("hello world"); } } @Test void testExplicitInitScripts() throws SQLException { try ( PostgreSQLContainer postgres = new PostgreSQLContainer(PostgreSQLTestImages.POSTGRES_TEST_IMAGE) .withInitScripts("somepath/init_postgresql.sql", "somepath/init_postgresql_2.sql") ) { postgres.start(); ResultSet resultSet = performQuery( postgres, "SELECT foo AS value FROM bar UNION SELECT bar AS value FROM foo" ); String columnValue1 = resultSet.getString(1); resultSet.next(); String columnValue2 = resultSet.getString(1); assertThat(columnValue1).as("Value from init script 1 should equal real value").isEqualTo("hello world"); assertThat(columnValue2).as("Value from init script 2 should equal real value").isEqualTo("hello world 2"); } } @Test void testWithAdditionalUrlParamInJdbcUrl() { try ( PostgreSQLContainer postgres = new PostgreSQLContainer(PostgreSQLTestImages.POSTGRES_TEST_IMAGE) .withUrlParam("charSet", "UNICODE") ) { postgres.start(); String jdbcUrl = postgres.getJdbcUrl(); assertThat(jdbcUrl).contains("?"); assertThat(jdbcUrl).contains("&"); assertThat(jdbcUrl).contains("charSet=UNICODE"); } } private void assertHasCorrectExposedAndLivenessCheckPorts(PostgreSQLContainer postgres) { assertThat(postgres.getExposedPorts()).containsExactly(PostgreSQLContainer.POSTGRESQL_PORT); assertThat(postgres.getLivenessCheckPortNumbers()) .containsExactly(postgres.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT)); } } ================================================ FILE: modules/postgresql/src/test/java/org/testcontainers/postgresql/PostgreSQLR2DBCDatabaseContainerTest.java ================================================ package org.testcontainers.postgresql; import io.r2dbc.spi.ConnectionFactoryOptions; import org.testcontainers.PostgreSQLTestImages; import org.testcontainers.r2dbc.AbstractR2DBCDatabaseContainerTest; public class PostgreSQLR2DBCDatabaseContainerTest extends AbstractR2DBCDatabaseContainerTest { @Override protected PostgreSQLContainer createContainer() { return new PostgreSQLContainer(PostgreSQLTestImages.POSTGRES_TEST_IMAGE); } @Override protected ConnectionFactoryOptions getOptions(PostgreSQLContainer container) { // spotless:off // get_options { ConnectionFactoryOptions options = PostgreSQLR2DBCDatabaseContainer.getOptions( container ); // } // spotless:on return options; } protected String createR2DBCUrl() { return "r2dbc:tc:postgresql:///db?TC_IMAGE_TAG=10-alpine"; } } ================================================ FILE: modules/postgresql/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/postgresql/src/test/resources/somepath/init_postgresql.sql ================================================ CREATE TABLE bar ( foo VARCHAR(255) ); INSERT INTO bar (foo) VALUES ('hello world'); ================================================ FILE: modules/postgresql/src/test/resources/somepath/init_postgresql_2.sql ================================================ CREATE TABLE foo ( bar VARCHAR(255) ); INSERT INTO foo (bar) VALUES ('hello world 2'); ================================================ FILE: modules/postgresql/src/test/resources/somepath/init_timescaledb.sql ================================================ -- Extend the database with TimescaleDB CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE; CREATE TABLE bar ( foo VARCHAR(255), time TIMESTAMPTZ NOT NULL ); SELECT create_hypertable('bar', 'time'); INSERT INTO bar (time, foo) VALUES (CURRENT_TIMESTAMP, 'hello world'); ================================================ FILE: modules/presto/build.gradle ================================================ description = "Testcontainers :: JDBC :: Presto" dependencies { api project(':testcontainers-jdbc') testImplementation project(':testcontainers-jdbc-test') testRuntimeOnly 'io.prestosql:presto-jdbc:350' compileOnly 'org.jetbrains:annotations:26.0.2-1' } ================================================ FILE: modules/presto/src/main/java/org/testcontainers/containers/PrestoContainer.java ================================================ package org.testcontainers.containers; import com.google.common.base.Strings; import org.jetbrains.annotations.NotNull; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import java.sql.Connection; import java.sql.SQLException; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.Set; /** * @deprecated Use {@code TrinoContainer} instead. */ @Deprecated public class PrestoContainer> extends JdbcDatabaseContainer { public static final String NAME = "presto"; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("ghcr.io/trinodb/presto"); public static final String IMAGE = "ghcr.io/trinodb/presto"; public static final String DEFAULT_TAG = "344"; public static final Integer PRESTO_PORT = 8080; private String username = "test"; private String catalog = null; /** * @deprecated use {@link #PrestoContainer(DockerImageName)} instead */ @Deprecated public PrestoContainer() { this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } public PrestoContainer(final String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public PrestoContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); waitingFor( Wait .forLogMessage(".*======== SERVER STARTED ========.*", 1) .withStartupTimeout(Duration.of(60, ChronoUnit.SECONDS)) ); addExposedPort(PRESTO_PORT); } /** * @return the ports on which to check if the container is ready * @deprecated use {@link #getLivenessCheckPortNumbers()} instead */ @NotNull @Override @Deprecated protected Set getLivenessCheckPorts() { return super.getLivenessCheckPorts(); } @Override public String getDriverClassName() { return "io.prestosql.jdbc.PrestoDriver"; } @Override public String getJdbcUrl() { return String.format( "jdbc:presto://%s:%s/%s", getHost(), getMappedPort(PRESTO_PORT), Strings.nullToEmpty(catalog) ); } @Override public String getUsername() { return username; } @Override public String getPassword() { return ""; } @Override public String getDatabaseName() { return catalog; } @Override public String getTestQueryString() { return "SELECT count(*) FROM tpch.tiny.nation"; } @Override public SELF withUsername(final String username) { this.username = username; return self(); } /** * @deprecated This operation is not supported. */ @Override @Deprecated public SELF withPassword(final String password) { // ignored, Presto does not support password authentication without TLS // TODO: make JDBCDriverTest not pass a password unconditionally and remove this method return self(); } @Override public SELF withDatabaseName(String dbName) { this.catalog = dbName; return self(); } public Connection createConnection() throws SQLException, NoDriverFoundException { return createConnection(""); } } ================================================ FILE: modules/presto/src/main/java/org/testcontainers/containers/PrestoContainerProvider.java ================================================ package org.testcontainers.containers; import org.testcontainers.jdbc.ConnectionUrl; import org.testcontainers.utility.DockerImageName; /** * Factory for Presto containers. */ public class PrestoContainerProvider extends JdbcDatabaseContainerProvider { public static final String USER_PARAM = "user"; public static final String PASSWORD_PARAM = "password"; @Override public boolean supports(String databaseType) { return databaseType.equals(PrestoContainer.NAME); } @Override public JdbcDatabaseContainer newInstance() { return newInstance(PrestoContainer.DEFAULT_TAG); } @Override public JdbcDatabaseContainer newInstance(String tag) { return new PrestoContainer(DockerImageName.parse(PrestoContainer.IMAGE).withTag(tag)); } @Override public JdbcDatabaseContainer newInstance(ConnectionUrl connectionUrl) { return newInstanceFromConnectionUrl(connectionUrl, USER_PARAM, PASSWORD_PARAM); } } ================================================ FILE: modules/presto/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider ================================================ org.testcontainers.containers.PrestoContainerProvider ================================================ FILE: modules/presto/src/test/java/org/testcontainers/PrestoTestImages.java ================================================ package org.testcontainers; import org.testcontainers.utility.DockerImageName; public interface PrestoTestImages { DockerImageName PRESTO_TEST_IMAGE = DockerImageName.parse("ghcr.io/trinodb/presto:344"); DockerImageName PRESTO_PREVIOUS_VERSION_TEST_IMAGE = DockerImageName.parse("ghcr.io/trinodb/presto:343"); } ================================================ FILE: modules/presto/src/test/java/org/testcontainers/containers/PrestoContainerTest.java ================================================ package org.testcontainers.containers; import org.junit.jupiter.api.Test; import org.testcontainers.PrestoTestImages; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; class PrestoContainerTest { @Test void testSimple() throws Exception { try (PrestoContainer prestoSql = new PrestoContainer<>(PrestoTestImages.PRESTO_TEST_IMAGE)) { prestoSql.start(); try ( Connection connection = prestoSql.createConnection(); Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery("SELECT DISTINCT node_version FROM system.runtime.nodes") ) { assertThat(resultSet.next()).as("has result").isTrue(); assertThat(resultSet.getString("node_version")) .as("Presto version") .isEqualTo(PrestoContainer.DEFAULT_TAG); assertHasCorrectExposedAndLivenessCheckPorts(prestoSql); } } } @Test void testSpecificVersion() throws Exception { try ( PrestoContainer prestoSql = new PrestoContainer<>(PrestoTestImages.PRESTO_PREVIOUS_VERSION_TEST_IMAGE) ) { prestoSql.start(); try ( Connection connection = prestoSql.createConnection(); Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery("SELECT DISTINCT node_version FROM system.runtime.nodes") ) { assertThat(resultSet.next()).as("has result").isTrue(); assertThat(resultSet.getString("node_version")) .as("Presto version") .isEqualTo(PrestoTestImages.PRESTO_PREVIOUS_VERSION_TEST_IMAGE.getVersionPart()); } } } @Test void testQueryMemoryAndTpch() throws SQLException { try (PrestoContainer prestoSql = new PrestoContainer<>(PrestoTestImages.PRESTO_TEST_IMAGE)) { prestoSql.start(); try ( Connection connection = prestoSql.createConnection(); Statement statement = connection.createStatement() ) { // Prepare data statement.execute( "CREATE TABLE memory.default.table_with_array AS SELECT 1 id, ARRAY[1, 42, 2, 42, 4, 42] my_array" ); // Query Presto using newly created table and a builtin connector try ( ResultSet resultSet = statement.executeQuery( "" + "SELECT nationkey, element " + "FROM tpch.tiny.nation " + "JOIN memory.default.table_with_array twa ON nationkey = twa.id " + "LEFT JOIN UNNEST(my_array) a(element) ON true " + "ORDER BY element OFFSET 1 FETCH NEXT 3 ROWS WITH TIES " ) ) { List actualElements = new ArrayList<>(); while (resultSet.next()) { actualElements.add(resultSet.getInt("element")); } assertThat(actualElements).isEqualTo(Arrays.asList(2, 4, 42, 42, 42)); } } } } @Test void testInitScript() throws Exception { try (PrestoContainer prestoSql = new PrestoContainer<>(PrestoTestImages.PRESTO_TEST_IMAGE)) { prestoSql.withInitScript("initial.sql"); prestoSql.start(); try ( Connection connection = prestoSql.createConnection(); Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery("SELECT a FROM memory.default.test_table") ) { assertThat(resultSet.next()).as("has result").isTrue(); assertThat(resultSet.getObject("a")).as("Value").isEqualTo(12345678909324L); assertThat(resultSet.next()).as("only has one result").isFalse(); } } } @Test void testTcJdbcUri() throws Exception { try ( Connection connection = DriverManager.getConnection( String.format("jdbc:tc:presto:%s://hostname/", PrestoContainer.DEFAULT_TAG) ) ) { // Verify metadata with tc: JDBC connection URI assertThat(Integer.parseInt(PrestoContainer.DEFAULT_TAG)) .isEqualTo(connection.getMetaData().getDatabaseMajorVersion()); // Verify transactions with tc: JDBC connection URI assertThat(connection.getAutoCommit()).as("Is autocommit").isTrue(); connection.setAutoCommit(false); assertThat(connection.getAutoCommit()).as("Is autocommit").isFalse(); assertThat(connection.getTransactionIsolation()) .as("Transaction isolation") .isEqualTo(Connection.TRANSACTION_READ_UNCOMMITTED); try (Statement statement = connection.createStatement()) { assertThat(statement.executeUpdate("CREATE TABLE memory.default.test_tc(a bigint)")) .as("Update result") .isEqualTo(0); try ( ResultSet resultSet = statement.executeQuery( "SELECT sum(cast(node_version AS bigint)) AS v FROM system.runtime.nodes" ) ) { assertThat(resultSet.next()).isTrue(); assertThat(resultSet.getString("v")).isEqualTo(PrestoContainer.DEFAULT_TAG); assertThat(resultSet.next()).isFalse(); } connection.commit(); } finally { connection.rollback(); } connection.setAutoCommit(true); assertThat(connection.getAutoCommit()).as("Is autocommit").isTrue(); assertThat(connection.getTransactionIsolation()) .as("Transaction isolation should be retained") .isEqualTo(Connection.TRANSACTION_READ_UNCOMMITTED); } } private void assertHasCorrectExposedAndLivenessCheckPorts(PrestoContainer prestoSql) { assertThat(prestoSql.getExposedPorts()).containsExactly(PrestoContainer.PRESTO_PORT); assertThat(prestoSql.getLivenessCheckPortNumbers()) .containsExactly(prestoSql.getMappedPort(PrestoContainer.PRESTO_PORT)); } } ================================================ FILE: modules/presto/src/test/java/org/testcontainers/jdbc/presto/PrestoJDBCDriverTest.java ================================================ package org.testcontainers.jdbc.presto; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; class PrestoJDBCDriverTest extends AbstractJDBCDriverTest { public static Iterable data() { return Arrays.asList( new Object[][] { // { "jdbc:tc:presto:344://hostname/", EnumSet.of(Options.PmdKnownBroken) }, } ); } } ================================================ FILE: modules/presto/src/test/resources/initial.sql ================================================ CREATE TABLE memory.default.test_table(a bigint); INSERT INTO memory.default.test_table(a) VALUES (12345678909324); ================================================ FILE: modules/presto/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/pulsar/build.gradle ================================================ description = "Testcontainers :: Pulsar" dependencies { api project(':testcontainers') testImplementation platform("org.apache.pulsar:pulsar-bom:4.1.1") testImplementation 'org.apache.pulsar:pulsar-client' testImplementation 'org.apache.pulsar:pulsar-client-admin' } ================================================ FILE: modules/pulsar/src/main/java/org/testcontainers/containers/PulsarContainer.java ================================================ package org.testcontainers.containers; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.containers.wait.strategy.WaitAllStrategy; import org.testcontainers.utility.DockerImageName; /** * Testcontainers implementation for Apache Pulsar. *

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

* Exposed ports: *

    *
  • Pulsar: 6650
  • *
  • HTTP: 8080
  • *
* * @deprecated use {@link org.testcontainers.pulsar.PulsarContainer} instead. */ @Deprecated public class PulsarContainer extends GenericContainer { public static final int BROKER_PORT = 6650; public static final int BROKER_HTTP_PORT = 8080; private static final String ADMIN_CLUSTERS_ENDPOINT = "/admin/v2/clusters"; /** * See SystemTopicNames. */ private static final String TRANSACTION_TOPIC_ENDPOINT = "/admin/v2/persistent/pulsar/system/transaction_coordinator_assign/partitions"; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("apachepulsar/pulsar"); @Deprecated private static final String DEFAULT_TAG = "3.0.0"; private final WaitAllStrategy waitAllStrategy = new WaitAllStrategy(); private boolean functionsWorkerEnabled = false; private boolean transactionsEnabled = false; /** * @deprecated use {@link #PulsarContainer(DockerImageName)} instead */ @Deprecated public PulsarContainer() { this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } /** * @deprecated use {@link #PulsarContainer(DockerImageName)} instead */ @Deprecated public PulsarContainer(String pulsarVersion) { this(DEFAULT_IMAGE_NAME.withTag(pulsarVersion)); } public PulsarContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, DockerImageName.parse("apachepulsar/pulsar-all")); withExposedPorts(BROKER_PORT, BROKER_HTTP_PORT); setWaitStrategy(waitAllStrategy); } @Override protected void configure() { super.configure(); setupCommandAndEnv(); } public PulsarContainer withFunctionsWorker() { functionsWorkerEnabled = true; return this; } public PulsarContainer withTransactions() { transactionsEnabled = true; return this; } public String getPulsarBrokerUrl() { return String.format("pulsar://%s:%s", getHost(), getMappedPort(BROKER_PORT)); } public String getHttpServiceUrl() { return String.format("http://%s:%s", getHost(), getMappedPort(BROKER_HTTP_PORT)); } protected void setupCommandAndEnv() { String standaloneBaseCommand = "/pulsar/bin/apply-config-from-env.py /pulsar/conf/standalone.conf " + "&& bin/pulsar standalone"; if (!functionsWorkerEnabled) { standaloneBaseCommand += " --no-functions-worker -nss"; } withCommand("/bin/bash", "-c", standaloneBaseCommand); final String clusterName = getEnvMap().getOrDefault("PULSAR_PREFIX_clusterName", "standalone"); final String response = String.format("[\"%s\"]", clusterName); waitAllStrategy.withStrategy( Wait.forHttp(ADMIN_CLUSTERS_ENDPOINT).forPort(BROKER_HTTP_PORT).forResponsePredicate(response::equals) ); if (transactionsEnabled) { withEnv("PULSAR_PREFIX_transactionCoordinatorEnabled", "true"); waitAllStrategy.withStrategy( Wait.forHttp(TRANSACTION_TOPIC_ENDPOINT).forStatusCode(200).forPort(BROKER_HTTP_PORT) ); } if (functionsWorkerEnabled) { waitAllStrategy.withStrategy(Wait.forLogMessage(".*Function worker service started.*", 1)); } } } ================================================ FILE: modules/pulsar/src/main/java/org/testcontainers/pulsar/PulsarContainer.java ================================================ package org.testcontainers.pulsar; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.containers.wait.strategy.WaitAllStrategy; import org.testcontainers.utility.DockerImageName; /** * Testcontainers implementation for Apache Pulsar. *

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

* Exposed ports: *

    *
  • Pulsar: 6650
  • *
  • HTTP: 8080
  • *
*/ public class PulsarContainer extends GenericContainer { public static final int BROKER_PORT = 6650; public static final int BROKER_HTTP_PORT = 8080; private static final String ADMIN_CLUSTERS_ENDPOINT = "/admin/v2/clusters"; /** * See SystemTopicNames. */ private static final String TRANSACTION_TOPIC_ENDPOINT = "/admin/v2/persistent/pulsar/system/transaction_coordinator_assign/partitions"; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("apachepulsar/pulsar"); private final WaitAllStrategy waitAllStrategy = new WaitAllStrategy(); private boolean functionsWorkerEnabled = false; private boolean transactionsEnabled = false; @Deprecated public PulsarContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public PulsarContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, DockerImageName.parse("apachepulsar/pulsar-all")); withExposedPorts(BROKER_PORT, BROKER_HTTP_PORT); setWaitStrategy(waitAllStrategy); } @Override protected void configure() { super.configure(); setupCommandAndEnv(); } public PulsarContainer withFunctionsWorker() { functionsWorkerEnabled = true; return this; } public PulsarContainer withTransactions() { transactionsEnabled = true; return this; } public String getPulsarBrokerUrl() { return String.format("pulsar://%s:%s", getHost(), getMappedPort(BROKER_PORT)); } public String getHttpServiceUrl() { return String.format("http://%s:%s", getHost(), getMappedPort(BROKER_HTTP_PORT)); } protected void setupCommandAndEnv() { String standaloneBaseCommand = "/pulsar/bin/apply-config-from-env.py /pulsar/conf/standalone.conf " + "&& bin/pulsar standalone"; if (!functionsWorkerEnabled) { standaloneBaseCommand += " --no-functions-worker -nss"; } withCommand("/bin/bash", "-c", standaloneBaseCommand); final String clusterName = getEnvMap().getOrDefault("PULSAR_PREFIX_clusterName", "standalone"); final String response = String.format("[\"%s\"]", clusterName); waitAllStrategy.withStrategy( Wait.forHttp(ADMIN_CLUSTERS_ENDPOINT).forPort(BROKER_HTTP_PORT).forResponsePredicate(response::equals) ); if (transactionsEnabled) { withEnv("PULSAR_PREFIX_transactionCoordinatorEnabled", "true"); waitAllStrategy.withStrategy( Wait.forHttp(TRANSACTION_TOPIC_ENDPOINT).forStatusCode(200).forPort(BROKER_HTTP_PORT) ); } if (functionsWorkerEnabled) { waitAllStrategy.withStrategy(Wait.forLogMessage(".*Function worker service started.*", 1)); } } } ================================================ FILE: modules/pulsar/src/test/java/org/testcontainers/pulsar/AbstractPulsar.java ================================================ package org.testcontainers.pulsar; 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"); } } ================================================ FILE: modules/pulsar/src/test/java/org/testcontainers/pulsar/CompatibleApachePulsarImageTest.java ================================================ package org.testcontainers.pulsar; import org.apache.pulsar.client.admin.PulsarAdmin; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.testcontainers.utility.DockerImageName; class CompatibleApachePulsarImageTest extends AbstractPulsar { public static String[] params() { return new String[] { "apachepulsar/pulsar:3.0.0", "apachepulsar/pulsar-all:3.0.0" }; } @ParameterizedTest @MethodSource("params") void testUsage(String imageName) throws Exception { try (PulsarContainer pulsar = new PulsarContainer(DockerImageName.parse(imageName));) { pulsar.start(); final String pulsarBrokerUrl = pulsar.getPulsarBrokerUrl(); testPulsarFunctionality(pulsarBrokerUrl); } } @ParameterizedTest @MethodSource("params") void testTransactions(String imageName) throws Exception { try (PulsarContainer pulsar = new PulsarContainer(DockerImageName.parse(imageName)).withTransactions();) { pulsar.start(); try (PulsarAdmin pulsarAdmin = PulsarAdmin.builder().serviceHttpUrl(pulsar.getHttpServiceUrl()).build()) { assertTransactionsTopicCreated(pulsarAdmin); } testTransactionFunctionality(pulsar.getPulsarBrokerUrl()); } } } ================================================ FILE: modules/pulsar/src/test/java/org/testcontainers/pulsar/PulsarContainerTest.java ================================================ package org.testcontainers.pulsar; import org.apache.pulsar.client.admin.PulsarAdmin; import org.apache.pulsar.client.admin.PulsarAdminException; import org.junit.jupiter.api.Test; import org.testcontainers.utility.DockerImageName; import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class PulsarContainerTest extends AbstractPulsar { private static final DockerImageName PULSAR_IMAGE = DockerImageName.parse("apachepulsar/pulsar:3.0.0"); @Test void testUsage() throws Exception { try ( // do not use PULSAR_IMAGE to make the doc looks easier // constructorWithVersion { PulsarContainer pulsar = new PulsarContainer("apachepulsar/pulsar:3.0.0"); // } ) { pulsar.start(); // coordinates { final String pulsarBrokerUrl = pulsar.getPulsarBrokerUrl(); final String httpServiceUrl = pulsar.getHttpServiceUrl(); // } testPulsarFunctionality(pulsarBrokerUrl); } } @Test void envVarsUsage() throws Exception { try ( // constructorWithEnv { PulsarContainer pulsar = new PulsarContainer(PULSAR_IMAGE) .withEnv("PULSAR_PREFIX_brokerDeduplicationEnabled", "true"); // } ) { pulsar.start(); testPulsarFunctionality(pulsar.getPulsarBrokerUrl()); } } @Test void customClusterName() throws Exception { try ( PulsarContainer pulsar = new PulsarContainer(PULSAR_IMAGE) .withEnv("PULSAR_PREFIX_clusterName", "tc-cluster"); ) { pulsar.start(); testPulsarFunctionality(pulsar.getPulsarBrokerUrl()); } } @Test void shouldNotEnableFunctionsWorkerByDefault() throws Exception { try (PulsarContainer pulsar = new PulsarContainer(PULSAR_IMAGE)) { pulsar.start(); try (PulsarAdmin pulsarAdmin = PulsarAdmin.builder().serviceHttpUrl(pulsar.getHttpServiceUrl()).build()) { assertThatThrownBy(() -> pulsarAdmin.functions().getFunctions("public", "default")) .isInstanceOf(PulsarAdminException.class); } } } @Test void shouldWaitForFunctionsWorkerStarted() throws Exception { try ( // constructorWithFunctionsWorker { PulsarContainer pulsar = new PulsarContainer(DockerImageName.parse("apachepulsar/pulsar:3.0.0")) .withFunctionsWorker(); // } ) { pulsar.start(); try (PulsarAdmin pulsarAdmin = PulsarAdmin.builder().serviceHttpUrl(pulsar.getHttpServiceUrl()).build()) { assertThat(pulsarAdmin.functions().getFunctions("public", "default")).hasSize(0); } } } @Test void testTransactions() throws Exception { try ( // constructorWithTransactions { PulsarContainer pulsar = new PulsarContainer(PULSAR_IMAGE).withTransactions(); // } ) { pulsar.start(); try (PulsarAdmin pulsarAdmin = PulsarAdmin.builder().serviceHttpUrl(pulsar.getHttpServiceUrl()).build()) { assertTransactionsTopicCreated(pulsarAdmin); } testTransactionFunctionality(pulsar.getPulsarBrokerUrl()); } } @Test void testTransactionsAndFunctionsWorker() throws Exception { try (PulsarContainer pulsar = new PulsarContainer(PULSAR_IMAGE).withTransactions().withFunctionsWorker()) { pulsar.start(); try (PulsarAdmin pulsarAdmin = PulsarAdmin.builder().serviceHttpUrl(pulsar.getHttpServiceUrl()).build();) { assertTransactionsTopicCreated(pulsarAdmin); assertThat(pulsarAdmin.functions().getFunctions("public", "default")).hasSize(0); } testTransactionFunctionality(pulsar.getPulsarBrokerUrl()); } } @Test void testClusterFullyInitialized() throws Exception { try (PulsarContainer pulsar = new PulsarContainer(PULSAR_IMAGE)) { pulsar.start(); try (PulsarAdmin pulsarAdmin = PulsarAdmin.builder().serviceHttpUrl(pulsar.getHttpServiceUrl()).build()) { assertThat(pulsarAdmin.clusters().getClusters()).hasSize(1).contains("standalone"); } } } @Test void testStartupTimeoutIsHonored() { try (PulsarContainer pulsar = new PulsarContainer(PULSAR_IMAGE).withStartupTimeout(Duration.ZERO)) { assertThatThrownBy(pulsar::start) .hasRootCauseMessage("Precondition failed: timeout must be greater than zero"); } } } ================================================ FILE: modules/pulsar/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/qdrant/build.gradle ================================================ description = "Testcontainers :: Qdrant" dependencies { api project(':testcontainers') testImplementation 'io.qdrant:client:1.16.1' testImplementation platform('io.grpc:grpc-bom:1.75.0') testImplementation 'io.grpc:grpc-stub' testImplementation 'io.grpc:grpc-protobuf' testImplementation 'io.grpc:grpc-netty-shaded' } ================================================ FILE: modules/qdrant/src/main/java/org/testcontainers/qdrant/QdrantContainer.java ================================================ package org.testcontainers.qdrant; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.DockerImageName; /** * Testcontainers implementation for Qdrant. *

* Supported image: {@code qdrant/qdrant} *

* Exposed ports: *

    *
  • HTTP: 6333
  • *
  • GRPC: 6334
  • *
*/ public class QdrantContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("qdrant/qdrant"); private static final int QDRANT_REST_PORT = 6333; private static final int QDRANT_GRPC_PORT = 6334; private static final String CONFIG_FILE_PATH = "/qdrant/config/config.yaml"; private static final String API_KEY_ENV = "QDRANT__SERVICE__API_KEY"; public QdrantContainer(String image) { this(DockerImageName.parse(image)); } public QdrantContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); withExposedPorts(QDRANT_REST_PORT, QDRANT_GRPC_PORT); waitingFor(Wait.forHttp("/readyz").forPort(QDRANT_REST_PORT)); } public QdrantContainer withApiKey(String apiKey) { return withEnv(API_KEY_ENV, apiKey); } public QdrantContainer withConfigFile(Transferable configFile) { return withCopyToContainer(configFile, CONFIG_FILE_PATH); } public int getGrpcPort() { return getMappedPort(QDRANT_GRPC_PORT); } public String getGrpcHostAddress() { return getHost() + ":" + getGrpcPort(); } } ================================================ FILE: modules/qdrant/src/test/java/org/testcontainers/qdrant/QdrantContainerTest.java ================================================ package org.testcontainers.qdrant; import io.qdrant.client.QdrantClient; import io.qdrant.client.QdrantGrpcClient; import io.qdrant.client.grpc.QdrantOuterClass; import org.junit.jupiter.api.Test; import org.testcontainers.images.builder.Transferable; import java.util.UUID; import java.util.concurrent.ExecutionException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class QdrantContainerTest { @Test void shouldReturnVersion() throws ExecutionException, InterruptedException { try ( // qdrantContainer { QdrantContainer qdrant = new QdrantContainer("qdrant/qdrant:v1.7.4") // } ) { qdrant.start(); QdrantClient client = new QdrantClient( QdrantGrpcClient.newBuilder(qdrant.getHost(), qdrant.getGrpcPort(), false).build() ); QdrantOuterClass.HealthCheckReply healthCheckReply = client.healthCheckAsync().get(); assertThat(healthCheckReply.getVersion()).isEqualTo("1.7.4"); client.close(); } } @Test void shouldSetApiKey() throws ExecutionException, InterruptedException { String apiKey = UUID.randomUUID().toString(); try (QdrantContainer qdrant = new QdrantContainer("qdrant/qdrant:v1.7.4").withApiKey(apiKey)) { qdrant.start(); final QdrantClient unauthClient = new QdrantClient( QdrantGrpcClient.newBuilder(qdrant.getHost(), qdrant.getGrpcPort(), false).build() ); assertThatThrownBy(() -> unauthClient.healthCheckAsync().get()).isInstanceOf(ExecutionException.class); unauthClient.close(); final QdrantClient client = new QdrantClient( QdrantGrpcClient.newBuilder(qdrant.getHost(), qdrant.getGrpcPort(), false).withApiKey(apiKey).build() ); QdrantOuterClass.HealthCheckReply healthCheckReply = client.healthCheckAsync().get(); assertThat(healthCheckReply.getVersion()).isEqualTo("1.7.4"); client.close(); } } @Test void shouldSetApiKeyUsingConfigFile() throws ExecutionException, InterruptedException { String apiKey = UUID.randomUUID().toString(); String configFile = "service:\n api_key: " + apiKey; try ( QdrantContainer qdrant = new QdrantContainer("qdrant/qdrant:v1.7.4") .withConfigFile(Transferable.of(configFile)) ) { qdrant.start(); final QdrantClient unauthClient = new QdrantClient( QdrantGrpcClient.newBuilder(qdrant.getHost(), qdrant.getGrpcPort(), false).build() ); assertThatThrownBy(() -> unauthClient.healthCheckAsync().get()).isInstanceOf(ExecutionException.class); unauthClient.close(); final QdrantClient client = new QdrantClient( QdrantGrpcClient.newBuilder(qdrant.getHost(), qdrant.getGrpcPort(), false).withApiKey(apiKey).build() ); QdrantOuterClass.HealthCheckReply healthCheckReply = client.healthCheckAsync().get(); assertThat(healthCheckReply.getVersion()).isEqualTo("1.7.4"); client.close(); } } } ================================================ FILE: modules/qdrant/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/questdb/build.gradle ================================================ description = "Testcontainers :: QuestDB" dependencies { api project(':testcontainers') api project(':testcontainers-jdbc') testRuntimeOnly 'org.postgresql:postgresql:42.7.8' testImplementation project(':testcontainers-jdbc-test') testImplementation 'org.questdb:questdb:9.2.2' testImplementation 'org.awaitility:awaitility:4.3.0' testImplementation 'org.apache.httpcomponents:httpclient:4.5.14' } ================================================ FILE: modules/questdb/src/main/java/org/testcontainers/containers/LegacyQuestDBProvider.java ================================================ package org.testcontainers.containers; @Deprecated public class LegacyQuestDBProvider extends JdbcDatabaseContainerProvider { @Override public boolean supports(String databaseType) { return databaseType.equals(QuestDBContainer.LEGACY_DATABASE_PROVIDER); } @Override public JdbcDatabaseContainer newInstance(String tag) { return new QuestDBContainer(QuestDBContainer.DEFAULT_IMAGE_NAME.withTag(tag)); } } ================================================ FILE: modules/questdb/src/main/java/org/testcontainers/containers/QuestDBContainer.java ================================================ package org.testcontainers.containers; import lombok.NonNull; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; /** * Testcontainers implementation for QuestDB. *

* Supported image: {@code questdb/questdb} *

* Exposed ports: *

    *
  • Postgres: 8812
  • *
  • HTTP: 9000
  • *
  • ILP: 9009
  • *
*/ public class QuestDBContainer extends JdbcDatabaseContainer { @Deprecated static final String LEGACY_DATABASE_PROVIDER = "postgresql"; static final String DATABASE_PROVIDER = "questdb"; private static final String DEFAULT_DATABASE_NAME = "qdb"; private static final int DEFAULT_COMMIT_LAG_MS = 1000; private static final String DEFAULT_USERNAME = "admin"; private static final String DEFAULT_PASSWORD = "quest"; private static final Integer POSTGRES_PORT = 8812; private static final Integer REST_PORT = 9000; private static final Integer ILP_PORT = 9009; static final String TEST_QUERY = "SELECT 1"; static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("questdb/questdb"); public QuestDBContainer(@NonNull String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public QuestDBContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); withExposedPorts(POSTGRES_PORT, REST_PORT, ILP_PORT); addEnv("QDB_CAIRO_COMMIT_LAG", String.valueOf(DEFAULT_COMMIT_LAG_MS)); waitingFor(Wait.forLogMessage("(?i).*A server-main enjoy.*", 1)); } @Override public String getDriverClassName() { return "org.postgresql.Driver"; } @Override public String getJdbcUrl() { return String.format("jdbc:postgresql://%s:%d/%s", getHost(), getMappedPort(8812), getDefaultDatabaseName()); } @Override public String getUsername() { return DEFAULT_USERNAME; } @Override public String getPassword() { return DEFAULT_PASSWORD; } @Override public String getTestQueryString() { return TEST_QUERY; } @Override protected void waitUntilContainerStarted() { getWaitStrategy().waitUntilReady(this); } public String getDefaultDatabaseName() { return DEFAULT_DATABASE_NAME; } public String getIlpUrl() { return getHost() + ":" + getMappedPort(ILP_PORT); } public String getHttpUrl() { return "http://" + getHost() + ":" + getMappedPort(REST_PORT); } } ================================================ FILE: modules/questdb/src/main/java/org/testcontainers/containers/QuestDBProvider.java ================================================ package org.testcontainers.containers; public class QuestDBProvider extends JdbcDatabaseContainerProvider { @Override public boolean supports(String databaseType) { return databaseType.equals(QuestDBContainer.DATABASE_PROVIDER); } @Override public JdbcDatabaseContainer newInstance(String tag) { return new QuestDBContainer(QuestDBContainer.DEFAULT_IMAGE_NAME.withTag(tag)); } } ================================================ FILE: modules/questdb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider ================================================ org.testcontainers.containers.LegacyQuestDBProvider org.testcontainers.containers.QuestDBProvider ================================================ FILE: modules/questdb/src/test/java/org/testcontainers/QuestDBTestImages.java ================================================ package org.testcontainers; import org.testcontainers.utility.DockerImageName; public interface QuestDBTestImages { DockerImageName QUESTDB_IMAGE = DockerImageName.parse("questdb/questdb:9.2.2"); } ================================================ FILE: modules/questdb/src/test/java/org/testcontainers/jdbc/questdb/QuestDBJDBCDriverTest.java ================================================ package org.testcontainers.jdbc.questdb; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; class QuestDBJDBCDriverTest extends AbstractJDBCDriverTest { public static Iterable data() { return Arrays.asList( new Object[][] { { "jdbc:tc:postgresql://hostname/databasename", EnumSet.of(Options.PmdKnownBroken) }, { "jdbc:tc:questdb://hostname/databasename", EnumSet.of(Options.PmdKnownBroken) }, } ); } } ================================================ FILE: modules/questdb/src/test/java/org/testcontainers/junit/questdb/SimpleQuestDBTest.java ================================================ package org.testcontainers.junit.questdb; import io.questdb.client.Sender; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; import org.junit.jupiter.api.Test; import org.testcontainers.QuestDBTestImages; import org.testcontainers.containers.QuestDBContainer; import org.testcontainers.db.AbstractContainerDatabaseTest; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.sql.ResultSet; import java.sql.SQLException; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; class SimpleQuestDBTest extends AbstractContainerDatabaseTest { private static final String TABLE_NAME = "mytable"; @Test void testSimple() throws SQLException { try ( // container { QuestDBContainer questDB = new QuestDBContainer("questdb/questdb:9.2.2") // } ) { questDB.start(); ResultSet resultSet = performQuery(questDB, questDB.getTestQueryString()); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); } } @Test void testRest() throws IOException { try (QuestDBContainer questdb = new QuestDBContainer(QuestDBTestImages.QUESTDB_IMAGE)) { questdb.start(); populateByInfluxLineProtocol(questdb, 1_000); try (CloseableHttpClient client = HttpClientBuilder.create().build()) { String encodedSql = URLEncoder.encode("select * from " + TABLE_NAME, "UTF-8"); HttpGet httpGet = new HttpGet(questdb.getHttpUrl() + "/exec?query=" + encodedSql); await() .untilAsserted(() -> { try (CloseableHttpResponse response = client.execute(httpGet)) { assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200); String json = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); assertThat(json.contains("\"count\":1000")).isTrue(); } }); } } } private static void populateByInfluxLineProtocol(QuestDBContainer questdb, int rowCount) { try (Sender sender = Sender.builder(Sender.Transport.TCP).address(questdb.getIlpUrl()).build()) { for (int i = 0; i < rowCount; i++) { sender .table(TABLE_NAME) .symbol("sym", "sym1" + i) .stringColumn("str", "str1" + i) .longColumn("long", i) .atNow(); } } } } ================================================ FILE: modules/questdb/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/r2dbc/build.gradle ================================================ plugins { id "java-test-fixtures" } description = "Testcontainers :: R2DBC" dependencies { api project(':testcontainers') api 'io.r2dbc:r2dbc-spi:0.9.0.RELEASE' testImplementation 'io.r2dbc:r2dbc-postgresql:0.8.13.RELEASE' testImplementation project(':testcontainers-postgresql') testFixturesImplementation 'io.projectreactor:reactor-core:3.8.1' testFixturesImplementation 'org.assertj:assertj-core:3.27.6' testFixturesImplementation 'org.junit.jupiter:junit-jupiter:5.14.1' } ================================================ FILE: modules/r2dbc/src/main/java/org/testcontainers/r2dbc/CancellableSubscription.java ================================================ package org.testcontainers.r2dbc; import org.reactivestreams.Subscription; import java.util.concurrent.atomic.AtomicBoolean; class CancellableSubscription implements Subscription { private final AtomicBoolean cancelled = new AtomicBoolean(); @Override public void request(long n) {} @Override public void cancel() { cancelled.set(true); } public boolean isCancelled() { return cancelled.get(); } } ================================================ FILE: modules/r2dbc/src/main/java/org/testcontainers/r2dbc/ConnectionPublisher.java ================================================ package org.testcontainers.r2dbc; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; /** * Design notes: * - ConnectionPublisher is Mono-like (0..1), the request amount is ignored * - given the testing nature, the performance requirements are less strict * - "synchronized" is used to avoid races * - Reactive Streams spec violations are not checked (e.g. non-positive request) */ class ConnectionPublisher implements Publisher { private final Supplier> futureSupplier; ConnectionPublisher(Supplier> futureSupplier) { this.futureSupplier = futureSupplier; } @Override public void subscribe(Subscriber actual) { actual.onSubscribe(new StateMachineSubscription(actual)); } private class StateMachineSubscription implements Subscription { private final Subscriber actual; Subscription subscriptionState; StateMachineSubscription(Subscriber actual) { this.actual = actual; subscriptionState = new WaitRequestSubscriptionState(); } @Override public synchronized void request(long n) { subscriptionState.request(n); } @Override public synchronized void cancel() { subscriptionState.cancel(); } synchronized void transitionTo(SubscriptionState newState) { subscriptionState = newState; newState.enter(); } abstract class SubscriptionState implements Subscription { void enter() {} } class WaitRequestSubscriptionState extends SubscriptionState { @Override public void request(long n) { transitionTo(new WaitFutureCompletionSubscriptionState()); } @Override public void cancel() {} } class WaitFutureCompletionSubscriptionState extends SubscriptionState { private CompletableFuture future; @Override void enter() { this.future = futureSupplier.get(); future.whenComplete((connectionFactory, e) -> { if (e != null) { actual.onSubscribe(EmptySubscription.INSTANCE); actual.onError(e); return; } Publisher publisher = connectionFactory.create(); transitionTo(new ProxySubscriptionState(publisher)); }); } @Override public void request(long n) {} @Override public void cancel() { future.cancel(true); } } class ProxySubscriptionState extends SubscriptionState implements Subscriber { private final Publisher publisher; private Subscription s; private boolean cancelled = false; ProxySubscriptionState(Publisher publisher) { this.publisher = publisher; } @Override void enter() { publisher.subscribe(this); } @Override public void request(long n) { // Ignore } @Override public synchronized void cancel() { cancelled = true; if (s != null) { s.cancel(); } } @Override public synchronized void onSubscribe(Subscription s) { this.s = s; if (!cancelled) { s.request(1); } else { s.cancel(); } } @Override public void onNext(Connection connection) { actual.onNext(connection); } @Override public void onError(Throwable t) { actual.onError(t); } @Override public void onComplete() { actual.onComplete(); } } } } ================================================ FILE: modules/r2dbc/src/main/java/org/testcontainers/r2dbc/EmptySubscription.java ================================================ package org.testcontainers.r2dbc; import org.reactivestreams.Subscription; enum EmptySubscription implements Subscription { INSTANCE; @Override public void request(long n) {} @Override public void cancel() {} } ================================================ FILE: modules/r2dbc/src/main/java/org/testcontainers/r2dbc/Hidden.java ================================================ package org.testcontainers.r2dbc; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.ConnectionFactoryOptions; import io.r2dbc.spi.ConnectionFactoryProvider; /** * Hide inner classes that must be public due to the way {@link java.util.ServiceLoader} works */ class Hidden { public static final class TestcontainersR2DBCConnectionFactoryProvider implements ConnectionFactoryProvider { public static final String DRIVER = "tc"; @Override public ConnectionFactory create(ConnectionFactoryOptions options) { options = sanitize(options); options = removeProxying(options); return new TestcontainersR2DBCConnectionFactory(options); } private ConnectionFactoryOptions sanitize(ConnectionFactoryOptions options) { ConnectionFactoryOptions.Builder builder = options.mutate(); Object reusable = options.getValue(R2DBCDatabaseContainerProvider.REUSABLE_OPTION); if (reusable instanceof String) { builder.option(R2DBCDatabaseContainerProvider.REUSABLE_OPTION, Boolean.valueOf((String) reusable)); } return builder.build(); } private ConnectionFactoryOptions removeProxying(ConnectionFactoryOptions options) { // To delegate to the next factory provider, inspect the PROTOCOL and convert it to the next DRIVER and PROTOCOL values. // // example: // | Property | Input | Output | // |----------|-----------------|--------------| // | DRIVER | tc | postgres | // | PROTOCOL | postgres | | String protocol = (String) options.getRequiredValue(ConnectionFactoryOptions.PROTOCOL); if (protocol.trim().length() == 0) { throw new IllegalArgumentException("Invalid protocol: " + protocol); } String[] protocols = protocol.split(":", 2); String driverDelegate = protocols[0]; // when protocol does NOT contain COLON, the length becomes 1 String protocolDelegate = protocols.length == 2 ? protocols[1] : ""; return ConnectionFactoryOptions .builder() .from(options) .option(ConnectionFactoryOptions.DRIVER, driverDelegate) .option(ConnectionFactoryOptions.PROTOCOL, protocolDelegate) .build(); } @Override public boolean supports(ConnectionFactoryOptions options) { return DRIVER.equals(options.getValue(ConnectionFactoryOptions.DRIVER)); } @Override public String getDriver() { return DRIVER; } } } ================================================ FILE: modules/r2dbc/src/main/java/org/testcontainers/r2dbc/R2DBCDatabaseContainer.java ================================================ package org.testcontainers.r2dbc; import io.r2dbc.spi.ConnectionFactoryOptions; import org.testcontainers.lifecycle.Startable; public interface R2DBCDatabaseContainer extends Startable { ConnectionFactoryOptions configure(ConnectionFactoryOptions options); } ================================================ FILE: modules/r2dbc/src/main/java/org/testcontainers/r2dbc/R2DBCDatabaseContainerProvider.java ================================================ package org.testcontainers.r2dbc; import io.r2dbc.spi.ConnectionFactories; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.ConnectionFactoryMetadata; import io.r2dbc.spi.ConnectionFactoryOptions; import io.r2dbc.spi.Option; import org.testcontainers.DockerClientFactory; import javax.annotation.Nullable; public interface R2DBCDatabaseContainerProvider { Option REUSABLE_OPTION = Option.valueOf("TC_REUSABLE"); Option IMAGE_TAG_OPTION = Option.valueOf("TC_IMAGE_TAG"); boolean supports(ConnectionFactoryOptions options); R2DBCDatabaseContainer createContainer(ConnectionFactoryOptions options); @Nullable default ConnectionFactoryMetadata getMetadata(ConnectionFactoryOptions options) { ConnectionFactoryOptions.Builder builder = options.mutate(); if (!options.hasOption(ConnectionFactoryOptions.HOST)) { builder.option(ConnectionFactoryOptions.HOST, DockerClientFactory.instance().dockerHostIpAddress()); } if (!options.hasOption(ConnectionFactoryOptions.PORT)) { builder.option(ConnectionFactoryOptions.PORT, 65535); } ConnectionFactory connectionFactory = ConnectionFactories.find(builder.build()); return connectionFactory != null ? connectionFactory.getMetadata() : null; } } ================================================ FILE: modules/r2dbc/src/main/java/org/testcontainers/r2dbc/TestcontainersR2DBCConnectionFactory.java ================================================ package org.testcontainers.r2dbc; import io.r2dbc.spi.Closeable; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactories; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.ConnectionFactoryMetadata; import io.r2dbc.spi.ConnectionFactoryOptions; import org.reactivestreams.Publisher; import org.testcontainers.lifecycle.Startable; import java.util.ServiceLoader; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.StreamSupport; class TestcontainersR2DBCConnectionFactory implements ConnectionFactory, Closeable { private static final AtomicLong THREAD_COUNT = new AtomicLong(); private static final Executor EXECUTOR = Executors.newCachedThreadPool(r -> { Thread thread = new Thread(r); thread.setName("testcontainers-r2dbc-" + THREAD_COUNT.getAndIncrement()); thread.setDaemon(true); return thread; }); private final ConnectionFactoryOptions options; private final R2DBCDatabaseContainerProvider containerProvider; private CompletableFuture future; TestcontainersR2DBCConnectionFactory(ConnectionFactoryOptions options) { this.options = options; containerProvider = StreamSupport .stream(ServiceLoader.load(R2DBCDatabaseContainerProvider.class).spliterator(), false) .filter(it -> it.supports(options)) .findAny() .orElseThrow(() -> new IllegalArgumentException("Missing provider for " + options)); } @Override public Publisher create() { return new ConnectionPublisher(() -> { if (future == null) { synchronized (this) { if (future == null) { future = CompletableFuture.supplyAsync( () -> { R2DBCDatabaseContainer container = containerProvider.createContainer(options); container.start(); return container; }, EXECUTOR ); } } } return future.thenApply(it -> { return ConnectionFactories.find(it.configure(options)); }); }); } @Override public ConnectionFactoryMetadata getMetadata() { return containerProvider.getMetadata(options); } @Override public Publisher close() { return s -> { CompletableFuture futureRef; synchronized (this) { futureRef = this.future; this.future = null; } CancellableSubscription subscription = new CancellableSubscription(); s.onSubscribe(subscription); if (futureRef == null) { if (!subscription.isCancelled()) { s.onComplete(); } } else { futureRef.thenAcceptAsync(Startable::stop, EXECUTOR); EXECUTOR.execute(() -> { futureRef.cancel(true); if (!subscription.isCancelled()) { s.onComplete(); } }); } }; } } ================================================ FILE: modules/r2dbc/src/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider ================================================ org.testcontainers.r2dbc.Hidden$TestcontainersR2DBCConnectionFactoryProvider ================================================ FILE: modules/r2dbc/src/test/java/org/testcontainers/r2dbc/TestcontainersR2DBCConnectionFactoryTest.java ================================================ package org.testcontainers.r2dbc; import io.r2dbc.postgresql.api.PostgresqlException; import io.r2dbc.spi.Closeable; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactories; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.Result; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class TestcontainersR2DBCConnectionFactoryTest { @Test void failsOnUnknownProvider() { String nonExistingProvider = UUID.randomUUID().toString(); assertThatThrownBy(() -> { ConnectionFactories.get(String.format("r2dbc:tc:%s:///db", nonExistingProvider)); }) .hasMessageContaining("Missing provider") .hasMessageContaining(nonExistingProvider); } @Test void reusesUntilConnectionFactoryIsClosed() { String url = "r2dbc:tc:postgresql:///db?TC_IMAGE_TAG=10-alpine"; ConnectionFactory connectionFactory = ConnectionFactories.get(url); Integer updated = Flux .usingWhen( connectionFactory.create(), connection -> { return Mono .from(connection.createStatement("CREATE TABLE test(id integer PRIMARY KEY)").execute()) .thenMany(connection.createStatement("INSERT INTO test(id) VALUES(123)").execute()) .flatMap(Result::getRowsUpdated); }, Connection::close ) .blockFirst(); assertThat(updated).isEqualTo(1); Flux select = Flux.usingWhen( Flux.defer(connectionFactory::create), connection -> { return Flux .from(connection.createStatement("SELECT COUNT(*) FROM test").execute()) .flatMap(it -> it.map((row, meta) -> (Long) row.get(0))); }, Connection::close ); Long rows = select.blockFirst(); assertThat(rows).isEqualTo(1); close(connectionFactory); assertThatThrownBy(select::blockFirst) .isInstanceOf(PostgresqlException.class) // relation "X" does not exists // https://github.com/postgres/postgres/blob/REL_10_0/src/backend/utils/errcodes.txt#L349 .returns("42P01", e -> ((PostgresqlException) e).getErrorDetails().getCode()); } private static void close(ConnectionFactory connectionFactory) { Mono.from(((Closeable) connectionFactory).close()).block(); } } ================================================ FILE: modules/r2dbc/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/r2dbc/src/testFixtures/java/org/testcontainers/r2dbc/AbstractR2DBCDatabaseContainerTest.java ================================================ package org.testcontainers.r2dbc; import io.r2dbc.spi.Closeable; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactories; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.ConnectionFactoryMetadata; import io.r2dbc.spi.ConnectionFactoryOptions; import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import static org.assertj.core.api.Assertions.assertThat; public abstract class AbstractR2DBCDatabaseContainerTest> { protected abstract ConnectionFactoryOptions getOptions(T container); protected abstract String createR2DBCUrl(); protected String query() { return "SELECT %d"; } protected String createTestQuery(int result) { return String.format(query(), result); } @Test void testGetOptions() { try (T container = createContainer()) { container.start(); ConnectionFactory connectionFactory = ConnectionFactories.get(getOptions(container)); runTestQuery(connectionFactory); } } @Test void testUrlSupport() { ConnectionFactory connectionFactory = ConnectionFactories.get(createR2DBCUrl()); runTestQuery(connectionFactory); } @Test void testGetMetadata() { ConnectionFactory connectionFactory = ConnectionFactories.get(createR2DBCUrl()); ConnectionFactoryMetadata metadata = connectionFactory.getMetadata(); assertThat(metadata).isNotNull(); } protected abstract T createContainer(); protected void runTestQuery(ConnectionFactory connectionFactory) { try { int expected = 42; Number result = Flux .usingWhen( connectionFactory.create(), connection -> connection.createStatement(createTestQuery(expected)).execute(), Connection::close ) .flatMap(it -> it.map((row, meta) -> (Number) row.get(0))) .blockFirst(); assertThat(result).isNotNull().returns(expected, Number::intValue); } finally { if (connectionFactory instanceof Closeable) { Mono.from(((Closeable) connectionFactory).close()).block(); } } } } ================================================ FILE: modules/rabbitmq/build.gradle ================================================ description = "Testcontainers :: RabbitMQ" dependencies { api project(":testcontainers") testImplementation 'com.rabbitmq:amqp-client:5.28.0' compileOnly 'org.jetbrains:annotations:26.0.2-1' } ================================================ FILE: modules/rabbitmq/src/main/java/org/testcontainers/containers/RabbitMQContainer.java ================================================ package org.testcontainers.containers; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.dockerjava.api.command.InspectContainerResponse; import org.jetbrains.annotations.NotNull; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; /** * Testcontainers implementation for RabbitMQ. *

* Supported image: {@code rabbitmq} *

* Exposed ports: *

    *
  • 5671 (AMQPS)
  • *
  • 5672 (AMQP)
  • *
  • 15671 (HTTPS)
  • *
  • 15672 (HTTP)
  • *
* * @deprecated use {@link org.testcontainers.rabbitmq.RabbitMQContainer} instead. */ @Deprecated public class RabbitMQContainer extends GenericContainer { /** * The image defaults to the official RabbitMQ image: RabbitMQ. */ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("rabbitmq"); private static final String DEFAULT_TAG = "3.7.25-management-alpine"; private static final int DEFAULT_AMQP_PORT = 5672; private static final int DEFAULT_AMQPS_PORT = 5671; private static final int DEFAULT_HTTPS_PORT = 15671; private static final int DEFAULT_HTTP_PORT = 15672; private String adminPassword = "guest"; private String adminUsername = "guest"; private final List> values = new ArrayList<>(); /** * Creates a RabbitMQ container using the official RabbitMQ docker image. * @deprecated use {@link #RabbitMQContainer(DockerImageName)} instead */ @Deprecated public RabbitMQContainer() { this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } /** * Creates a RabbitMQ container using a specific docker image. * * @param dockerImageName The docker image to use. */ public RabbitMQContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public RabbitMQContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); addExposedPorts(DEFAULT_AMQP_PORT, DEFAULT_AMQPS_PORT, DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT); waitingFor(Wait.forLogMessage(".*Server startup complete.*", 1)); } @Override protected void configure() { if (this.adminUsername != null) { addEnv("RABBITMQ_DEFAULT_USER", this.adminUsername); } if (this.adminPassword != null) { addEnv("RABBITMQ_DEFAULT_PASS", this.adminPassword); } } @Override protected void containerIsStarted(InspectContainerResponse containerInfo) { values.forEach(command -> { try { ExecResult execResult = execInContainer(command.toArray(new String[0])); if (execResult.getExitCode() != 0) { logger().error("Could not execute command {}: {}", command, execResult.getStderr()); } } catch (IOException | InterruptedException e) { logger().error("Could not execute command {}: {}", command, e.getMessage()); } }); } /** * @return The admin password for the admin account */ public String getAdminPassword() { return this.adminPassword; } /** * @return The admin user for the admin account */ public String getAdminUsername() { return this.adminUsername; } public Integer getAmqpPort() { return getMappedPort(DEFAULT_AMQP_PORT); } public Integer getAmqpsPort() { return getMappedPort(DEFAULT_AMQPS_PORT); } public Integer getHttpsPort() { return getMappedPort(DEFAULT_HTTPS_PORT); } public Integer getHttpPort() { return getMappedPort(DEFAULT_HTTP_PORT); } /** * @return AMQP URL for use with AMQP clients. */ public String getAmqpUrl() { return "amqp://" + getHost() + ":" + getAmqpPort(); } /** * @return AMQPS URL for use with AMQPS clients. */ public String getAmqpsUrl() { return "amqps://" + getHost() + ":" + getAmqpsPort(); } /** * @return URL of the HTTP management endpoint. */ public String getHttpUrl() { return "http://" + getHost() + ":" + getHttpPort(); } /** * @return URL of the HTTPS management endpoint. */ 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
) * * @param adminPassword The admin password. * @return This container. */ public RabbitMQContainer withAdminPassword(final String adminPassword) { this.adminPassword = adminPassword; return this; } public enum SslVerification { VERIFY_NONE("verify_none"), VERIFY_PEER("verify_peer"); SslVerification(String value) { this.value = value; } private final String value; } public RabbitMQContainer withSSL( final MountableFile keyFile, final MountableFile certFile, final MountableFile caFile, final SslVerification verify, boolean failIfNoCert, int verificationDepth ) { return withSSL(keyFile, certFile, caFile, verify, failIfNoCert) .withEnv("RABBITMQ_SSL_DEPTH", String.valueOf(verificationDepth)); } public RabbitMQContainer withSSL( final MountableFile keyFile, final MountableFile certFile, final MountableFile caFile, final SslVerification verify, boolean failIfNoCert ) { return withSSL(keyFile, certFile, caFile, verify) .withEnv("RABBITMQ_SSL_FAIL_IF_NO_PEER_CERT", String.valueOf(failIfNoCert)); } public RabbitMQContainer withSSL( final MountableFile keyFile, final MountableFile certFile, final MountableFile caFile, final SslVerification verify ) { return withEnv("RABBITMQ_SSL_CACERTFILE", "/etc/rabbitmq/ca_cert.pem") .withEnv("RABBITMQ_SSL_CERTFILE", "/etc/rabbitmq/rabbitmq_cert.pem") .withEnv("RABBITMQ_SSL_KEYFILE", "/etc/rabbitmq/rabbitmq_key.pem") .withEnv("RABBITMQ_SSL_VERIFY", verify.value) .withCopyFileToContainer(certFile, "/etc/rabbitmq/rabbitmq_cert.pem") .withCopyFileToContainer(caFile, "/etc/rabbitmq/ca_cert.pem") .withCopyFileToContainer(keyFile, "/etc/rabbitmq/rabbitmq_key.pem"); } /** * @deprecated use {@link #execInContainer(String...)} instead */ @Deprecated public RabbitMQContainer withPluginsEnabled(String... pluginNames) { List command = new ArrayList<>(Arrays.asList("rabbitmq-plugins", "enable")); command.addAll(Arrays.asList(pluginNames)); values.add(command); return self(); } /** * @deprecated use {@link #execInContainer(String...)} instead */ @Deprecated public RabbitMQContainer withBinding(String source, String destination) { values.add( Arrays.asList("rabbitmqadmin", "declare", "binding", "source=" + source, "destination=" + destination) ); return self(); } /** * @deprecated use {@link #execInContainer(String...)} instead */ @Deprecated public RabbitMQContainer withBinding(String vhost, String source, String destination) { values.add( Arrays.asList( "rabbitmqadmin", "--vhost=" + vhost, "declare", "binding", "source=" + source, "destination=" + destination ) ); return self(); } /** * @deprecated use {@link #execInContainer(String...)} instead */ @Deprecated public RabbitMQContainer withBinding( String source, String destination, Map arguments, String routingKey, String destinationType ) { values.add( Arrays.asList( "rabbitmqadmin", "declare", "binding", "source=" + source, "destination=" + destination, "routing_key=" + routingKey, "destination_type=" + destinationType, "arguments=" + toJson(arguments) ) ); return self(); } /** * @deprecated use {@link #execInContainer(String...)} instead */ @Deprecated public RabbitMQContainer withBinding( String vhost, String source, String destination, Map arguments, String routingKey, String destinationType ) { values.add( Arrays.asList( "rabbitmqadmin", "--vhost=" + vhost, "declare", "binding", "source=" + source, "destination=" + destination, "routing_key=" + routingKey, "destination_type=" + destinationType, "arguments=" + toJson(arguments) ) ); return self(); } /** * @deprecated use {@link #execInContainer(String...)} instead */ @Deprecated public RabbitMQContainer withParameter(String component, String name, String value) { values.add( Arrays.asList( "rabbitmqadmin", "declare", "parameter", "component=" + component, "name=" + name, "value=" + value ) ); return self(); } /** * @deprecated use {@link #execInContainer(String...)} instead */ @Deprecated public RabbitMQContainer withPermission(String vhost, String user, String configure, String write, String read) { values.add( Arrays.asList( "rabbitmqadmin", "declare", "permission", "vhost=" + vhost, "user=" + user, "configure=" + configure, "write=" + write, "read=" + read ) ); return self(); } /** * @deprecated use {@link #execInContainer(String...)} instead */ @Deprecated public RabbitMQContainer withUser(String name, String password) { values.add(Arrays.asList("rabbitmqadmin", "declare", "user", "name=" + name, "password=" + password, "tags=")); return self(); } /** * @deprecated use {@link #execInContainer(String...)} instead */ @Deprecated public RabbitMQContainer withUser(String name, String password, Set tags) { values.add( Arrays.asList( "rabbitmqadmin", "declare", "user", "name=" + name, "password=" + password, "tags=" + String.join(",", tags) ) ); return self(); } /** * @deprecated use {@link #execInContainer(String...)} instead */ @Deprecated public RabbitMQContainer withPolicy(String name, String pattern, Map definition) { values.add( Arrays.asList( "rabbitmqadmin", "declare", "policy", "name=" + name, "pattern=" + pattern, "definition=" + toJson(definition) ) ); return self(); } /** * @deprecated use {@link #execInContainer(String...)} instead */ @Deprecated public RabbitMQContainer withPolicy(String vhost, String name, String pattern, Map definition) { values.add( Arrays.asList( "rabbitmqadmin", "declare", "policy", "--vhost=" + vhost, "name=" + name, "pattern=" + pattern, "definition=" + toJson(definition) ) ); return self(); } /** * @deprecated use {@link #execInContainer(String...)} instead */ @Deprecated public RabbitMQContainer withPolicy( String name, String pattern, Map definition, int priority, String applyTo ) { values.add( Arrays.asList( "rabbitmqadmin", "declare", "policy", "name=" + name, "pattern=" + pattern, "priority=" + priority, "apply-to=" + applyTo, "definition=" + toJson(definition) ) ); return self(); } /** * @deprecated use {@link #execInContainer(String...)} instead */ @Deprecated public RabbitMQContainer withOperatorPolicy(String name, String pattern, Map definition) { values.add( new ArrayList<>( Arrays.asList( "rabbitmqadmin", "declare", "operator_policy", "name=" + name, "pattern=" + pattern, "definition=" + toJson(definition) ) ) ); return self(); } /** * @deprecated use {@link #execInContainer(String...)} instead */ @Deprecated public RabbitMQContainer withOperatorPolicy( String name, String pattern, Map definition, int priority, String applyTo ) { values.add( Arrays.asList( "rabbitmqadmin", "declare", "operator_policy", "name=" + name, "pattern=" + pattern, "priority=" + priority, "apply-to=" + applyTo, "definition=" + toJson(definition) ) ); return self(); } /** * @deprecated use {@link #execInContainer(String...)} instead */ @Deprecated public RabbitMQContainer withVhost(String name) { values.add(Arrays.asList("rabbitmqadmin", "declare", "vhost", "name=" + name)); return self(); } /** * @deprecated use {@link #execInContainer(String...)} instead */ @Deprecated public RabbitMQContainer withVhost(String name, boolean tracing) { values.add(Arrays.asList("rabbitmqadmin", "declare", "vhost", "name=" + name, "tracing=" + tracing)); return self(); } /** * @deprecated use {@link #execInContainer(String...)} instead */ @Deprecated public RabbitMQContainer withVhostLimit(String vhost, String name, int value) { values.add( Arrays.asList("rabbitmqadmin", "declare", "vhost_limit", "vhost=" + vhost, "name=" + name, "value=" + value) ); return self(); } /** * @deprecated use {@link #execInContainer(String...)} instead */ @Deprecated public RabbitMQContainer withQueue(String name) { values.add(Arrays.asList("rabbitmqadmin", "declare", "queue", "name=" + name)); return self(); } /** * @deprecated use {@link #execInContainer(String...)} instead */ @Deprecated public RabbitMQContainer withQueue(String vhost, String name) { values.add(Arrays.asList("rabbitmqadmin", "--vhost=" + vhost, "declare", "queue", "name=" + name)); return self(); } /** * @deprecated use {@link #execInContainer(String...)} instead */ @Deprecated public RabbitMQContainer withQueue( String name, boolean autoDelete, boolean durable, Map arguments ) { values.add( Arrays.asList( "rabbitmqadmin", "declare", "queue", "name=" + name, "auto_delete=" + autoDelete, "durable=" + durable, "arguments=" + toJson(arguments) ) ); return self(); } /** * @deprecated use {@link #execInContainer(String...)} instead */ @Deprecated public RabbitMQContainer withQueue( String vhost, String name, boolean autoDelete, boolean durable, Map arguments ) { values.add( Arrays.asList( "rabbitmqadmin", "--vhost=" + vhost, "declare", "queue", "name=" + name, "auto_delete=" + autoDelete, "durable=" + durable, "arguments=" + toJson(arguments) ) ); return self(); } /** * @deprecated use {@link #execInContainer(String...)} instead */ @Deprecated public RabbitMQContainer withExchange(String name, String type) { values.add(Arrays.asList("rabbitmqadmin", "declare", "exchange", "name=" + name, "type=" + type)); return self(); } /** * @deprecated use {@link #execInContainer(String...)} instead */ @Deprecated public RabbitMQContainer withExchange(String vhost, String name, String type) { values.add( Arrays.asList("rabbitmqadmin", "--vhost=" + vhost, "declare", "exchange", "name=" + name, "type=" + type) ); return self(); } /** * @deprecated use {@link #execInContainer(String...)} instead */ @Deprecated public RabbitMQContainer withExchange( String name, String type, boolean autoDelete, boolean internal, boolean durable, Map arguments ) { values.add( Arrays.asList( "rabbitmqadmin", "declare", "exchange", "name=" + name, "type=" + type, "auto_delete=" + autoDelete, "internal=" + internal, "durable=" + durable, "arguments=" + toJson(arguments) ) ); return self(); } /** * @deprecated use {@link #execInContainer(String...)} instead */ @Deprecated public RabbitMQContainer withExchange( String vhost, String name, String type, boolean autoDelete, boolean internal, boolean durable, Map arguments ) { values.add( Arrays.asList( "rabbitmqadmin", "--vhost=" + vhost, "declare", "exchange", "name=" + name, "type=" + type, "auto_delete=" + autoDelete, "internal=" + internal, "durable=" + durable, "arguments=" + toJson(arguments) ) ); return self(); } /** * Overwrites the default RabbitMQ configuration file with the supplied one. * * @param rabbitMQConf The rabbitmq.conf file to use (in sysctl format, don't forget empty line in the end of file) * @return This container. */ public RabbitMQContainer withRabbitMQConfig(MountableFile rabbitMQConf) { return withRabbitMQConfigSysctl(rabbitMQConf); } /** * Overwrites the default RabbitMQ configuration file with the supplied one. * * This function doesn't work with RabbitMQ < 3.7. * * This function and the Sysctl format is recommended for RabbitMQ >= 3.7 * * @param rabbitMQConf The rabbitmq.config file to use (in sysctl format, don't forget empty line in the end of file) * @return This container. */ public RabbitMQContainer withRabbitMQConfigSysctl(MountableFile rabbitMQConf) { withEnv("RABBITMQ_CONFIG_FILE", "/etc/rabbitmq/rabbitmq-custom.conf"); return withCopyFileToContainer(rabbitMQConf, "/etc/rabbitmq/rabbitmq-custom.conf"); } /** * Overwrites the default RabbitMQ configuration file with the supplied one. * * @param rabbitMQConf The rabbitmq.config file to use (in erlang format) * @return This container. */ public RabbitMQContainer withRabbitMQConfigErlang(MountableFile rabbitMQConf) { withEnv("RABBITMQ_CONFIG_FILE", "/etc/rabbitmq/rabbitmq-custom.config"); return withCopyFileToContainer(rabbitMQConf, "/etc/rabbitmq/rabbitmq-custom.config"); } @NotNull private String toJson(Map arguments) { try { return new ObjectMapper().writeValueAsString(arguments); } catch (JsonProcessingException e) { throw new RuntimeException("Failed to convert arguments into json: " + e.getMessage(), e); } } } ================================================ FILE: modules/rabbitmq/src/main/java/org/testcontainers/rabbitmq/RabbitMQContainer.java ================================================ package org.testcontainers.rabbitmq; import com.github.dockerjava.api.command.InspectContainerResponse; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * Testcontainers implementation for RabbitMQ. *

* Supported image: {@code rabbitmq} *

* Exposed ports: *

    *
  • 5671 (AMQPS)
  • *
  • 5672 (AMQP)
  • *
  • 15671 (HTTPS)
  • *
  • 15672 (HTTP)
  • *
*/ public class RabbitMQContainer extends GenericContainer { /** * The image defaults to the official RabbitMQ image: RabbitMQ. */ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("rabbitmq"); private static final int DEFAULT_AMQP_PORT = 5672; private static final int DEFAULT_AMQPS_PORT = 5671; private static final int DEFAULT_HTTPS_PORT = 15671; private static final int DEFAULT_HTTP_PORT = 15672; private String adminPassword = "guest"; private String adminUsername = "guest"; private final List> values = new ArrayList<>(); /** * Creates a RabbitMQ container using a specific docker image. * * @param dockerImageName The docker image to use. */ public RabbitMQContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public RabbitMQContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); addExposedPorts(DEFAULT_AMQP_PORT, DEFAULT_AMQPS_PORT, DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT); waitingFor(Wait.forLogMessage(".*Server startup complete.*", 1)); } @Override protected void configure() { if (this.adminUsername != null) { addEnv("RABBITMQ_DEFAULT_USER", this.adminUsername); } if (this.adminPassword != null) { addEnv("RABBITMQ_DEFAULT_PASS", this.adminPassword); } } @Override protected void containerIsStarted(InspectContainerResponse containerInfo) { values.forEach(command -> { try { ExecResult execResult = execInContainer(command.toArray(new String[0])); if (execResult.getExitCode() != 0) { logger().error("Could not execute command {}: {}", command, execResult.getStderr()); } } catch (IOException | InterruptedException e) { logger().error("Could not execute command {}: {}", command, e.getMessage()); } }); } /** * @return The admin password for the admin account */ public String getAdminPassword() { return this.adminPassword; } /** * @return The admin user for the admin account */ public String getAdminUsername() { return this.adminUsername; } public Integer getAmqpPort() { return getMappedPort(DEFAULT_AMQP_PORT); } public Integer getAmqpsPort() { return getMappedPort(DEFAULT_AMQPS_PORT); } public Integer getHttpsPort() { return getMappedPort(DEFAULT_HTTPS_PORT); } public Integer getHttpPort() { return getMappedPort(DEFAULT_HTTP_PORT); } /** * @return AMQP URL for use with AMQP clients. */ public String getAmqpUrl() { return "amqp://" + getHost() + ":" + getAmqpPort(); } /** * @return AMQPS URL for use with AMQPS clients. */ public String getAmqpsUrl() { return "amqps://" + getHost() + ":" + getAmqpsPort(); } /** * @return URL of the HTTP management endpoint. */ public String getHttpUrl() { return "http://" + getHost() + ":" + getHttpPort(); } /** * @return URL of the HTTPS management endpoint. */ 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
) * * @param adminPassword The admin password. * @return This container. */ public RabbitMQContainer withAdminPassword(final String adminPassword) { this.adminPassword = adminPassword; return this; } /** * Overwrites the default RabbitMQ configuration file with the supplied one. * * @param rabbitMQConf The rabbitmq.conf file to use (in sysctl format, don't forget empty line in the end of file) * @return This container. */ public RabbitMQContainer withRabbitMQConfig(MountableFile rabbitMQConf) { return withRabbitMQConfigSysctl(rabbitMQConf); } /** * Overwrites the default RabbitMQ configuration file with the supplied one. * * This function doesn't work with RabbitMQ < 3.7. * * This function and the Sysctl format is recommended for RabbitMQ >= 3.7 * * @param rabbitMQConf The rabbitmq.config file to use (in sysctl format, don't forget empty line in the end of file) * @return This container. */ public RabbitMQContainer withRabbitMQConfigSysctl(MountableFile rabbitMQConf) { withEnv("RABBITMQ_CONFIG_FILE", "/etc/rabbitmq/rabbitmq-custom.conf"); return withCopyFileToContainer(rabbitMQConf, "/etc/rabbitmq/rabbitmq-custom.conf"); } /** * Overwrites the default RabbitMQ configuration file with the supplied one. * * @param rabbitMQConf The rabbitmq.config file to use (in erlang format) * @return This container. */ public RabbitMQContainer withRabbitMQConfigErlang(MountableFile rabbitMQConf) { withEnv("RABBITMQ_CONFIG_FILE", "/etc/rabbitmq/rabbitmq-custom.config"); return withCopyFileToContainer(rabbitMQConf, "/etc/rabbitmq/rabbitmq-custom.config"); } } ================================================ FILE: modules/rabbitmq/src/test/java/org/testcontainers/rabbitmq/RabbitMQContainerTest.java ================================================ package org.testcontainers.rabbitmq; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.DeliverCallback; import org.junit.jupiter.api.Test; import org.testcontainers.utility.MountableFile; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeoutException; import static org.assertj.core.api.Assertions.assertThat; class RabbitMQContainerTest { public static final int DEFAULT_AMQPS_PORT = 5671; public static final int DEFAULT_AMQP_PORT = 5672; public static final int DEFAULT_HTTPS_PORT = 15671; public static final int DEFAULT_HTTP_PORT = 15672; @Test void shouldCreateRabbitMQContainer() { try (RabbitMQContainer container = new RabbitMQContainer(RabbitMQTestImages.RABBITMQ_IMAGE)) { container.start(); assertThat(container.getAdminPassword()).isEqualTo("guest"); assertThat(container.getAdminUsername()).isEqualTo("guest"); assertThat(container.getAmqpsUrl()) .isEqualTo(String.format("amqps://%s:%d", container.getHost(), container.getAmqpsPort())); assertThat(container.getAmqpUrl()) .isEqualTo(String.format("amqp://%s:%d", container.getHost(), container.getAmqpPort())); assertThat(container.getHttpsUrl()) .isEqualTo(String.format("https://%s:%d", container.getHost(), container.getHttpsPort())); assertThat(container.getHttpUrl()) .isEqualTo(String.format("http://%s:%d", container.getHost(), container.getHttpPort())); assertThat(container.getLivenessCheckPortNumbers()) .containsExactlyInAnyOrder( container.getMappedPort(DEFAULT_AMQP_PORT), container.getMappedPort(DEFAULT_AMQPS_PORT), container.getMappedPort(DEFAULT_HTTP_PORT), container.getMappedPort(DEFAULT_HTTPS_PORT) ); assertFunctionality(container); } } @Test 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); } } @Test void shouldMountConfigurationFile() { try (RabbitMQContainer container = new RabbitMQContainer(RabbitMQTestImages.RABBITMQ_IMAGE)) { container.withRabbitMQConfig(MountableFile.forClasspathResource("/rabbitmq-custom.conf")); container.start(); assertThat(container.getLogs()).contains("debug"); // config file changes log level to `debug` } } @Test void shouldMountConfigurationFileErlang() { try (RabbitMQContainer container = new RabbitMQContainer(RabbitMQTestImages.RABBITMQ_IMAGE)) { container.withRabbitMQConfigErlang(MountableFile.forClasspathResource("/rabbitmq-custom.config")); container.start(); assertThat(container.getLogs()).contains("debug"); // config file changes log level to `debug` } } @Test void shouldMountConfigurationFileSysctl() { try (RabbitMQContainer container = new RabbitMQContainer(RabbitMQTestImages.RABBITMQ_IMAGE)) { container.withRabbitMQConfigSysctl(MountableFile.forClasspathResource("/rabbitmq-custom.conf")); container.start(); assertThat(container.getLogs()).contains("debug"); // config file changes log level to `debug` } } 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); } } } ================================================ FILE: modules/rabbitmq/src/test/java/org/testcontainers/rabbitmq/RabbitMQTestImages.java ================================================ package org.testcontainers.rabbitmq; import org.testcontainers.utility.DockerImageName; public interface RabbitMQTestImages { DockerImageName RABBITMQ_IMAGE = DockerImageName.parse("rabbitmq:3.7.25-management-alpine"); } ================================================ FILE: modules/rabbitmq/src/test/resources/certs/ca_certificate.pem ================================================ -----BEGIN CERTIFICATE----- MIIDRzCCAi+gAwIBAgIJAJJIMzvZuRzlMA0GCSqGSIb3DQEBCwUAMDExIDAeBgNV BAMMF1RMU0dlblNlbGZTaWduZWR0Um9vdENBMQ0wCwYDVQQHDAQkJCQkMCAXDTE5 MDUwMjA3MjI0OVoYDzIxMTkwNDA4MDcyMjQ5WjAxMSAwHgYDVQQDDBdUTFNHZW5T ZWxmU2lnbmVkdFJvb3RDQTENMAsGA1UEBwwEJCQkJDCCASIwDQYJKoZIhvcNAQEB BQADggEPADCCAQoCggEBAKko8FmfzrLHyZckvdR1oiSZf80m0t66TMqtLat1Oxjh CjsxvswwJ/m2I5dM48hwZ+0b2ufkvaudLPq/8jDGyONVfjMGlbe1YlmQMDC7YWdI XM1nCWAZIKaOHwIkfswuVBAdBVYV4Polu6wjVt5edEpl/IWEpPicXjLOY1Fw3q67 5tP2Mmo6TJg5YqgB4fH4SmajtP3j+H4puQ8ZPIs26mInEgfCyrMWey/oQX8qqMph pKMEJYE7DHawriFraOooJadJYojbY5H27nmJe8yXURb3wSQSaKnFZL25cmVm2kue /lw+n+a2wLdHdU4cmghCURalhcXUNZe7UbdRZ9e9r2cCAwEAAaNgMF4wCwYDVR0P BAQDAgEGMB0GA1UdDgQWBBSZiNur/XHsqSfdWnB1NPi/ql5+tzAfBgNVHSMEGDAW gBSZiNur/XHsqSfdWnB1NPi/ql5+tzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 DQEBCwUAA4IBAQAar/db/T7izD4pyh2titl7Dkgp2iTditfgqRlU0yVGiiB6rLmY sYE2QAuFhgqyRLPcjVV8F39iRJHQ17SGT8e2iAaUTnbQj0AiskKjonF9+quKuVbr TpYHk+guS0Jn2rU6HK8WQeYZOh3WdLTu4ArXkxywgwVssQQ9JmpTd9YEYePWfs7i WZB6AQyL9CD3z1j4i1G4ft6pB1Ps5XjznqMZ2//7AUpoRTrettWqorPWwudQ9yna B4S6KtvpnxUQSeHJW6Q4NvTrOsvHEOCa6OtwYbWmLf+qbpPb8oHt9UF3ze2PJopB QzsQop1+gPudG0DX0SgyuQT+SsFjYlDazZdZ -----END CERTIFICATE----- ================================================ FILE: modules/rabbitmq/src/test/resources/certs/ca_key.pem ================================================ -----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCpKPBZn86yx8mX JL3UdaIkmX/NJtLeukzKrS2rdTsY4Qo7Mb7MMCf5tiOXTOPIcGftG9rn5L2rnSz6 v/IwxsjjVX4zBpW3tWJZkDAwu2FnSFzNZwlgGSCmjh8CJH7MLlQQHQVWFeD6Jbus I1beXnRKZfyFhKT4nF4yzmNRcN6uu+bT9jJqOkyYOWKoAeHx+Epmo7T94/h+KbkP GTyLNupiJxIHwsqzFnsv6EF/KqjKYaSjBCWBOwx2sK4ha2jqKCWnSWKI22OR9u55 iXvMl1EW98EkEmipxWS9uXJlZtpLnv5cPp/mtsC3R3VOHJoIQlEWpYXF1DWXu1G3 UWfXva9nAgMBAAECggEAVHUVM5o/aDGp6+WzRa2Jy/47ueEFbaDUkGjQoYeBfxV0 t0NKAMaWXu3abUM9gyjkKpU6wYcKT/HEsFk/gazuRdq8jJtgCv3r4c3E0b/sjNWr R/6Gxs0k6SOSRc6U5DrJS9ZBgM6hqiNGxVZLm/DK3Q54eu1UNLBVs8Yp/lJ9S/3C oM6lhex6SoCTiZyFb3ZvVTmBAZe/ROFXZAtmVac1KORBFN8FpkXQcB3EPTkvZmNJ sSp8dVPBLABm6gAbdJRLVqqndkHDPsopFGHLGzo4LtR7AlEklfxBdC/XlYmudZeY peWba+ZdLH3sCybE55Xcm7nTfRRSQSLDv++r73GCCQKBgQDe/7ltr8N+gWpb1fDC nEyBU4WHoTRp09sD+MZe3B06PX3WNRrf+3wHj9oB9UkhZ1baCq84mRzBiUKpAYrw GrC4iL4kqbWeIZzcJqLZ7zk1zzHzW0+zzNJSjzE8dq3W26+SnuLqKDFpkxtAyIMQ Rp9AZJIW8q9ay6ZCsZk/NNBoKwKBgQDCMYh53dXKiYtr6tAXXManWWpQ24af6WKl 3faXCw5eXG7ic5bNZD7ql+DWJzRylkI4yeLDvWAsHprtyIBYmkRki3lBAnpLeHY8 7XWjZeOVeIpiSbG+1Fo7LrOuWi3wYGOZ61QUTjqJhvZJpzZBMEgo7cx+WQhlrioI 9BbXqPabtQKBgAZJv7jQE/slOxKL3dYfAilDaaiBazDwwGRER5O1MT3LLhk0NiXK uZyc+dDEUeOXPmO3mWlHKABtFmwdlwVeO014zaPLBUwINpwemsj6beqOhSIPmRfA 1s1tLD5AOnasiy7fPBbOO1Z2x3X0MX3r/+GY9GWhQkCVLYMD7wZRPu8xAoGBAJMr PNW2s+ZJtPq1Orzp17dOAU+D/xPDqLoxbEbt3xbOEE7X8Mp5lWDudzt0/L92dntZ LNzQ8UiebSWVlQcQ6pIUTXFiMlJt2ZW1FDkf54kIkD+KwATyI+vEKfIRb81DD1i/ yrmUy7IcMRyCd5CRya4TAa4jRUTh6ANfEMyhxTsdAoGBAIFub7nikxPQ8J4ItWq8 EKOptyziJ38KnL2nZnFyVDu6uZDtSk9Tf1dHqWy8D4yP9K3OLshipEp7rnNG25dr e7fkB58K77eSoaEpCZmwzPZstwkXbeBl+DHerDyC2UBXwIUWWFq+FuCdRQqCoYmU OGiaT92l+UInN7VchZjJS7Oj -----END PRIVATE KEY----- ================================================ FILE: modules/rabbitmq/src/test/resources/certs/client_certificate.pem ================================================ -----BEGIN CERTIFICATE----- MIIDKDCCAhCgAwIBAgIBAjANBgkqhkiG9w0BAQsFADAxMSAwHgYDVQQDDBdUTFNH ZW5TZWxmU2lnbmVkdFJvb3RDQTENMAsGA1UEBwwEJCQkJDAgFw0xOTA1MDIwNzIy NDlaGA8yMTE5MDQwODA3MjI0OVowIzEQMA4GA1UEAwwHQzY1U1RUMjEPMA0GA1UE CgwGY2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtcAse1RO mRngQ4NNFFgyPgbq2eZc3c/ERKdg2xKi2XESMIbOreVkoBRq1IXoxkJHgQZJvPP8 JbH7hmY+DFZcreszlEk5FI7bvxFNQM3CKsy2LeEcN9BTchSEsWgmSbwiZJ31kEB8 u/i+/GpgTTxtwPWi/zTep+7fKM4dyzYX9WcMu8wif3jKIzqXVUqHpWopFUK9zLQf JRnyfiD2uYbfcZYopsIG+wdL2Ebgfr8fpc+YsWn0aA0zBiEFAKYh9bewPqDIZKyg qd4FKzeUaqGHynttNxKDJ9z0oD84PKFEOuryf7V8ODU8+kI1+y/+3C5Hlq6n92y6 eSPn81W44+WZeQIDAQABo1cwVTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIFoDATBgNV HSUEDDAKBggrBgEFBQcDAjAmBgNVHREEHzAdggdDNjVTVFQyggdDNjVTVFQyggls b2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggEBAH3oqsmr+hIfMEpZUolPNuAl43TF KUu14SeFyS+P8fEIQVGkHdpEpHzC0G+fvREAy4YSw0WE8upOq6tkbbjen222SP0J C2vj0B9cCPLZUQ4kW6Z7JJD/1qryML0XB9uMPHqDeV/yBETJZW80TV0f7p8GWKXe +spu1W13rJ0jvQQd9anNEx3VhcKTwBIcTAhTHothxFLLhX1zb/oVZcl1riBopBFm WtrZRTCHs31o5GBnIs540uH0f2iCk7wUf8M/evxv5F1hms+NV7kfOz7fyCEXc9La P9pDjuxTNOUeOZH815BR9Lj71txY9AdjXqEXj7JdecACLsjHc+LMEP36cls= -----END CERTIFICATE----- ================================================ FILE: modules/rabbitmq/src/test/resources/certs/client_key.pem ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEAtcAse1ROmRngQ4NNFFgyPgbq2eZc3c/ERKdg2xKi2XESMIbO reVkoBRq1IXoxkJHgQZJvPP8JbH7hmY+DFZcreszlEk5FI7bvxFNQM3CKsy2LeEc N9BTchSEsWgmSbwiZJ31kEB8u/i+/GpgTTxtwPWi/zTep+7fKM4dyzYX9WcMu8wi f3jKIzqXVUqHpWopFUK9zLQfJRnyfiD2uYbfcZYopsIG+wdL2Ebgfr8fpc+YsWn0 aA0zBiEFAKYh9bewPqDIZKygqd4FKzeUaqGHynttNxKDJ9z0oD84PKFEOuryf7V8 ODU8+kI1+y/+3C5Hlq6n92y6eSPn81W44+WZeQIDAQABAoIBAQCooWMkEnbSakW/ niV4CNSk5DombiwfyVOq9zlQSZw670QXLhy5D6srM4ZjJNNyj7BUMAdef2mld9uN OXO8cqyO2TkEDmQdhOayAlWRGNdcao9lRgWua2Xg5NSw3ZcYtquae0yJyKtypDpf bDtprfWPINlYvC8R1PnMnGDcWJYmIyQyQuB2OJVCJ4ayhS4miBk91fR0T0yMhmlZ RKZWbECvQirwA1podKvVSpwhAEUlLkVWRqVVYkiadWoXc8u6Dg2NrI9KNCgk2/3W m8sVC9VgjkXw5L8cToTBmTrjRS831O0JR1iDuZIzbwliJhGtVcHtiTUvybu3boYL R4F9Z4fhAoGBAN0h+7yBIr7TvCXyURfemWrcWBYWmMwlr6GdcE0WACnUUiFL7aQs 0Mo5DWnwUG74hk1E8TG6oRU9oJs+69295cXe1m6wUwZySh4qlqfFEwnB2PzZwGrS FnRrCnJGiyLfY5v5hFlzWl/APtuxXGMa3S+ehbtFBhvLfN6vmSPpBTzNAoGBANJo iM6AK+ldkbi9SCES8zMWPLNWrEh7SiA+bItyoKOY275TUUxVLz0MP2S1B2yGo9Og fe6fWGeXfMgcL7+1q2cwMuieIaG5Qcs4u+3qJNVmerf5PlMJUkN7D3Prw1C73SLq cQVaRXs4LBpmMC5ajOODrZt/GdrRrn2D13kQKI9dAoGAaqTW+MP2c709Qbeo8DAE IQr+2DgxnFKYbwK0hBiWH5Yrva8WflS2pK/7Dho9UCc+7cjP4UG2Kb481GH18kyA oXqkQ2F5yOQZZo73dRWP5ua7tMV3DI0hEygEM7RdqYW+Thx5fYIqFX9rURwqCAmO nkZ/DB9voLv0Dpj06+KXCgkCgYAmigck680fPYhHckQX6sSpAtWzc5iy3gJBza1M DX7m+ESno9MsTB4O7INgCtiFRFQVmzv1zTIAJ3svnBoS30+54tYwTWaTnL80Xfvu JAkDHXY05G5J/1cWDSBTd0ebLg3fK1nwRQyc+Tj6zOTeWK+drKzL4of10JpJWzDI d/E18QKBgQC9IIZ5oc30NL5Ie5ypcHG4xrim6OR0akeDDeZfV6BrhOnpdi8l4aIC Jswilj/PQF657s5trujxN3YxUeCunCtuOqcrE0O5AAL4CTQGy3LIq4IGYWGV/19I ZTyM0Bf5iPEpVYLTflf9wW2UElPw4Uz2+ky0mKBhtRBYMAYAR0NvfA== -----END RSA PRIVATE KEY----- ================================================ FILE: modules/rabbitmq/src/test/resources/certs/server_certificate.pem ================================================ -----BEGIN CERTIFICATE----- MIIDajCCAlKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAxMSAwHgYDVQQDDBdUTFNH ZW5TZWxmU2lnbmVkdFJvb3RDQTENMAsGA1UEBwwEJCQkJDAgFw0xOTA1MDIwNzIy NDlaGA8yMTE5MDQwODA3MjI0OVowIzEQMA4GA1UEAwwHQzY1U1RUMjEPMA0GA1UE CgwGc2VydmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqR0QXKtb KVeEuCmZGcZAlAlTBC8E/G3UuX6qKwTR1xEOvUWeBH1n0WeXXGd/p/y6P4lRBeWN BZ9KcvIlNDeDMy05NfxnO1vnJk9E8/0xwMiY1LJdMHzIzhmrrqXo0u3DT8MmoNR6 7CTcnG21gi1GrjW8a747yFF0xfukEc6FkyVqLsjtCkHPwrc/sBHVS3aivNWGkJzA eBXBdWJAg3ZC6T9U+Y8cndWQrpYMJvek1IewlyDSspHZDFmM1OwVwypnMt4fGgaX 5IlUMnNgKmisOSuI529rxLF+mvYIQLRl5bP+1/c9JD5MZ5krA3SrjdwRFS3sQXC3 nuHqJofFXNkbXQIDAQABo4GYMIGVMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgWgMBMG A1UdJQQMMAoGCCsGAQUFBwMBMCYGA1UdEQQfMB2CB0M2NVNUVDKCB0M2NVNUVDKC CWxvY2FsaG9zdDAdBgNVHQ4EFgQURq22sa46tA0SGHhEm9jxGP9aDrswHwYDVR0j BBgwFoAUmYjbq/1x7Kkn3VpwdTT4v6pefrcwDQYJKoZIhvcNAQELBQADggEBAKUP 7RgmJyMVoHxg46F1fjWVhlF4BbQuEtB8mC+4G4e68lDU/TPAbmB3aj91oQDgBiTd R2O7U6tyitxxrU2r7rFAHGhFHeyCQ3yZMwydO2V3Nm2Ywzdyk8er4yghjg9FS8tH egDGDDod3l1yrAbHHuXmzDjnAFwHwRkm5cYUz00/IuZ3sQZ70XofL3KXNj1tAtfK PSpdSAxSTO99ofjVKjlyywQSZKNbXfqD5DGz8e0rmqPfZ+3zi75E5nEuJ3UI2wXg LuI4j6FIzNQyei/FdSynktcIm+hefQEyex4cho4C8RYB2S5S8RWrnP9jOzsaQFHn bHXf7dKwRfA6/u8JmtQ= -----END CERTIFICATE----- ================================================ FILE: modules/rabbitmq/src/test/resources/certs/server_key.pem ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAqR0QXKtbKVeEuCmZGcZAlAlTBC8E/G3UuX6qKwTR1xEOvUWe BH1n0WeXXGd/p/y6P4lRBeWNBZ9KcvIlNDeDMy05NfxnO1vnJk9E8/0xwMiY1LJd MHzIzhmrrqXo0u3DT8MmoNR67CTcnG21gi1GrjW8a747yFF0xfukEc6FkyVqLsjt CkHPwrc/sBHVS3aivNWGkJzAeBXBdWJAg3ZC6T9U+Y8cndWQrpYMJvek1IewlyDS spHZDFmM1OwVwypnMt4fGgaX5IlUMnNgKmisOSuI529rxLF+mvYIQLRl5bP+1/c9 JD5MZ5krA3SrjdwRFS3sQXC3nuHqJofFXNkbXQIDAQABAoIBAA0dxvYZCEIFmrKZ 71jzanDQ5FJvvyhA8H3OmC4r+oZ+uTDu5FmezF2OdkvhbyI9VMi2wsT9T9m+yAxw QXhyUce3WzeXsv4Em8H55fQykBhOtqPQja/EDeMGVK2ACrXJYRufnDBfKoWEOmQb kjddgZzjaBDHOWXJA5CTet8ysGOAJBTxyzU69k5Vj9B5abG9CofNzGOFF+Uleff5 ip3sz7JpDXCex3oEs98veco6+8i/MZNo3BnwB5J+P+2MFFKONfPwuNyKAWBza2/X 66Lk3xXBjLJJ+Ww16jkqueTXEq6GCFXavNfdL9aonth5V5YYR/cj+2u2LM1oj9cJ bp0xbvUCgYEA2Svq1DyR9cfTwrbc/0J2JfrjavClzDYU2oeO2fSU85WEEjJguaja 17Vdo/UsJtiUiSq4UhI1n0haaIpTBCeF2tHGXVEYZ7ZBi1zzdWbWlDxFmi+rcE57 ytx5w+iLE366tQEMa/Jn3bly54pG5JZAr9TXkpg9sMbzWZri2ocyU/cCgYEAx1l/ 9X9C/OruDp/MhhmVwKfw/X2+RhZRuv0pPcpJu7/gIoLgaxNj41XSeLqLYMlisaRk GFU17GFXtfRGE1a3z+jj8UPTP2sHk3w8m0yI+pgWgsvG0TJ0B+XsRfpVxFiIoaEs 3AsBaGR+hrRY1dpaJ9Cu3J9mEeToTpbCzPzVDksCgYEAzwSvWNvYY4u2UFHSvz2S tMfBzCpUUiNno50/ToN5De4ENPhy/eh5nNDVz7qh+PHSPiNMC2gyV4E4NZlOY5Jt Zdc8ma35brvtJTVZGxwKBsqhqsYwTeFy3kFnjZn6IX5X6r1yIuCzpEfowdEtnS+h wDtLuAGKJR6x0UP1Zk0ka6cCgYBGE6I1rJzhx7wTi/0bjtbjuKWwlolSnfnxH5ll zTyKMXMa7qLxQQm2Gq84HWtthJ2bEMzW+O1RwQ5SOiKAHdXT0mx+nXcfLgKlx+CO PyNP5DLVm8iyNWgwdpTOLKgFs5GkL8JTP9Mo3VrVA4TO+EkFAgjWKXp6A9vd9IVa Be7nbQKBgAVtFKuf9nbCMfN+W1gN0vlW2lwxCTa4w0KHgIlGIIvnYVuixSgu9fGt uylQcQirEjqrdzdVF9L2BQ37ZcLaGh1LoCmx8XVCX/HhbwW2RP798P3Z1P7htm16 ha5OfuPjHvoZklbYJo6EORJZQehS2VP63pjdnmUeMHPFzrPUevI5 -----END RSA PRIVATE KEY----- ================================================ FILE: modules/rabbitmq/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/rabbitmq/src/test/resources/rabbitmq-custom.conf ================================================ log.console.level = debug ================================================ FILE: modules/rabbitmq/src/test/resources/rabbitmq-custom.config ================================================ [ {rabbit, [ {log, [{console, [{level, debug}]}] } ] } ]. ================================================ FILE: modules/redpanda/build.gradle ================================================ description = "Testcontainers :: Redpanda" dependencies { api project(':testcontainers') shaded 'org.freemarker:freemarker:2.3.34' testImplementation 'org.apache.kafka:kafka-clients:4.1.1' testImplementation 'io.rest-assured:rest-assured:5.5.6' testImplementation 'org.awaitility:awaitility:4.3.0' } ================================================ FILE: modules/redpanda/src/main/java/org/testcontainers/redpanda/RedpandaContainer.java ================================================ package org.testcontainers.redpanda; import com.github.dockerjava.api.command.InspectContainerResponse; import freemarker.template.Configuration; import freemarker.template.Template; import lombok.AllArgsConstructor; import lombok.Cleanup; import lombok.Data; import lombok.SneakyThrows; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.ComparableVersion; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.io.ByteArrayOutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; /** * Testcontainers implementation for Redpanda. *

* Supported images: {@code redpandadata/redpanda}, {@code docker.redpanda.com/redpandadata/redpanda} *

* Exposed ports: *

    *
  • Broker: 9092
  • *
  • Schema Registry: 8081
  • *
  • Proxy: 8082
  • *
*/ public class RedpandaContainer extends GenericContainer { private static final String REDPANDA_FULL_IMAGE_NAME = "docker.redpanda.com/redpandadata/redpanda"; private static final String IMAGE_NAME = "redpandadata/redpanda"; private static final DockerImageName REDPANDA_IMAGE = DockerImageName.parse(REDPANDA_FULL_IMAGE_NAME); private static final DockerImageName IMAGE = DockerImageName.parse(IMAGE_NAME); private static final int REDPANDA_PORT = 9092; private static final int REDPANDA_ADMIN_PORT = 9644; private static final int SCHEMA_REGISTRY_PORT = 8081; private static final int REST_PROXY_PORT = 8082; private boolean enableAuthorization; private String authenticationMethod = "none"; private String schemaRegistryAuthenticationMethod = "none"; private final List superusers = new ArrayList<>(); @Deprecated private final Set> listenersValueSupplier = new HashSet<>(); private final Map> listeners = new HashMap<>(); public RedpandaContainer(String image) { this(DockerImageName.parse(image)); } public RedpandaContainer(DockerImageName imageName) { super(imageName); imageName.assertCompatibleWith(REDPANDA_IMAGE, IMAGE); boolean isLessThanBaseVersion = new ComparableVersion(imageName.getVersionPart()).isLessThan("v22.2.1"); boolean isPublicCompatibleImage = REDPANDA_FULL_IMAGE_NAME.equals(imageName.getUnversionedPart()) || IMAGE_NAME.equals(imageName.getUnversionedPart()); if (isPublicCompatibleImage && isLessThanBaseVersion) { throw new IllegalArgumentException("Redpanda version must be >= v22.2.1"); } withExposedPorts(REDPANDA_PORT, REDPANDA_ADMIN_PORT, SCHEMA_REGISTRY_PORT, REST_PROXY_PORT); withCreateContainerCmdModifier(cmd -> { cmd.withEntrypoint("/entrypoint-tc.sh"); cmd.withUser("root:root"); }); waitingFor(Wait.forLogMessage(".*Successfully started Redpanda!.*", 1)); withCopyFileToContainer( MountableFile.forClasspathResource("testcontainers/entrypoint-tc.sh", 0700), "/entrypoint-tc.sh" ); withCommand("redpanda", "start", "--mode=dev-container", "--smp=1", "--memory=1G"); } @Override protected void configure() { this.listenersValueSupplier.stream() .map(Supplier::get) .map(Listener::getAddress) .forEach(this::withNetworkAliases); this.listeners.keySet().stream().map(listener -> listener.split(":")[0]).forEach(this::withNetworkAliases); } @SneakyThrows @Override protected void containerIsStarting(InspectContainerResponse containerInfo) { super.containerIsStarting(containerInfo); Configuration cfg = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS); cfg.setClassLoaderForTemplateLoading(getClass().getClassLoader(), "testcontainers"); cfg.setDefaultEncoding("UTF-8"); copyFileToContainer(getBootstrapFile(cfg), "/etc/redpanda/.bootstrap.yaml"); copyFileToContainer(getRedpandaFile(cfg), "/etc/redpanda/redpanda.yaml"); } /** * Returns the bootstrap servers address. * @return the bootstrap servers address */ public String getBootstrapServers() { return String.format("PLAINTEXT://%s:%s", getHost(), getMappedPort(REDPANDA_PORT)); } /** * Returns the schema registry address. * @return the schema registry address */ public String getSchemaRegistryAddress() { return String.format("http://%s:%s", getHost(), getMappedPort(SCHEMA_REGISTRY_PORT)); } /** * Returns the admin address. * @return the admin address */ public String getAdminAddress() { return String.format("http://%s:%s", getHost(), getMappedPort(REDPANDA_ADMIN_PORT)); } /** * Returns the rest proxy address. * @return the rest proxy address */ public String getRestProxyAddress() { return String.format("http://%s:%s", getHost(), getMappedPort(REST_PROXY_PORT)); } /** * Enables authorization. * @return this {@link RedpandaContainer} instance */ public RedpandaContainer enableAuthorization() { this.enableAuthorization = true; return this; } /** * Enables SASL. * @return this {@link RedpandaContainer} instance */ public RedpandaContainer enableSasl() { this.authenticationMethod = "sasl"; return this; } /** * Enables Http Basic Auth for Schema Registry. * @return this {@link RedpandaContainer} instance */ public RedpandaContainer enableSchemaRegistryHttpBasicAuth() { this.schemaRegistryAuthenticationMethod = "http_basic"; return this; } /** * Register username as a superuser. * @param username username to register as a superuser * @return this {@link RedpandaContainer} instance */ public RedpandaContainer withSuperuser(String username) { this.superusers.add(username); return this; } /** * Add a {@link Supplier} that will provide a listener with format {@code host:port}. * Host will be added as a network alias. *

* The listener will be added to the default listeners. *

* Default listeners: *

    *
  • 0.0.0.0:9092
  • *
  • 0.0.0.0:9093
  • *
*

* Default advertised listeners: *

    *
  • {@code container.getHost():container.getMappedPort(9092)}
  • *
  • 127.0.0.1:9093
  • *
* @param listenerSupplier a supplier that will provide a listener * @return this {@link RedpandaContainer} instance * @deprecated use {@link #withListener(String, Supplier)} instead */ @Deprecated public RedpandaContainer withListener(Supplier listenerSupplier) { String[] parts = listenerSupplier.get().split(":"); this.listenersValueSupplier.add(() -> new Listener(parts[0], Integer.parseInt(parts[1]))); return this; } /** * Add a listener in the format {@code host:port}. * Host will be included as a network alias. *

* Use it to register additional connections to the Kafka broker within the same container network. *

* The listener will be added to the list of default listeners. *

* Default listeners: *

    *
  • 0.0.0.0:9092
  • *
  • 0.0.0.0:9093
  • *
*

* The listener will be added to the list of default advertised listeners. *

* Default advertised listeners: *

    *
  • {@code container.getConfig().getHostName():9092}
  • *
  • {@code container.getHost():container.getMappedPort(9093)}
  • *
* @param listener a listener with format {@code host:port} * @return this {@link RedpandaContainer} instance */ public RedpandaContainer withListener(String listener) { this.listeners.put(listener, () -> listener); return this; } /** * Add a listener in the format {@code host:port} and a {@link Supplier} for the advertised listener. * Host from listener will be included as a network alias. *

* Use it to register additional connections to the Kafka broker from outside the container network *

* The listener will be added to the list of default listeners. *

* Default listeners: *

    *
  • 0.0.0.0:9092
  • *
  • 0.0.0.0:9093
  • *
*

* The {@link Supplier} will be added to the list of default advertised listeners. *

* Default advertised listeners: *

    *
  • {@code container.getConfig().getHostName():9092}
  • *
  • {@code container.getHost():container.getMappedPort(9093)}
  • *
* @param listener a supplier that will provide a listener * @param advertisedListener a supplier that will provide a listener * @return this {@link RedpandaContainer} instance */ public RedpandaContainer withListener(String listener, Supplier advertisedListener) { this.listeners.put(listener, advertisedListener); return this; } private Transferable getBootstrapFile(Configuration cfg) { Map kafkaApi = new HashMap<>(); kafkaApi.put("enableAuthorization", this.enableAuthorization); kafkaApi.put("superusers", this.superusers); Map root = new HashMap<>(); root.put("kafkaApi", kafkaApi); String file = resolveTemplate(cfg, "bootstrap.yaml.ftl", root); return Transferable.of(file, 0700); } private Transferable getRedpandaFile(Configuration cfg) { Map kafkaApi = new HashMap<>(); kafkaApi.put("authenticationMethod", this.authenticationMethod); kafkaApi.put("enableAuthorization", this.enableAuthorization); kafkaApi.put("advertisedHost", getHost()); kafkaApi.put("advertisedPort", getMappedPort(9092)); List> listeners = this.listenersValueSupplier.stream() .map(Supplier::get) .map(listener -> { Map listenerMap = new HashMap<>(); listenerMap.put("address", listener.getAddress()); listenerMap.put("port", listener.getPort()); listenerMap.put("authentication_method", this.authenticationMethod); return listenerMap; }) .collect(Collectors.toList()); kafkaApi.put("listeners", listeners); List> kafkaListeners = this.listeners.keySet() .stream() .map(listener -> { Map listenerMap = new HashMap<>(); listenerMap.put("name", listener.split(":")[0]); listenerMap.put("address", listener.split(":")[0]); listenerMap.put("port", listener.split(":")[1]); listenerMap.put("authentication_method", this.authenticationMethod); return listenerMap; }) .collect(Collectors.toList()); List> kafkaAdvertisedListeners = this.listeners.entrySet() .stream() .map(entry -> { String advertisedListener = entry.getValue().get(); Map listenerMap = new HashMap<>(); listenerMap.put("name", entry.getKey().split(":")[0]); listenerMap.put("address", advertisedListener.split(":")[0]); listenerMap.put("port", advertisedListener.split(":")[1]); return listenerMap; }) .collect(Collectors.toList()); Map kafka = new HashMap<>(); kafka.put("listeners", kafkaListeners); kafka.put("advertisedListeners", kafkaAdvertisedListeners); Map schemaRegistry = new HashMap<>(); schemaRegistry.put("authenticationMethod", this.schemaRegistryAuthenticationMethod); Map root = new HashMap<>(); root.put("kafkaApi", kafkaApi); root.put("kafka", kafka); root.put("schemaRegistry", schemaRegistry); String file = resolveTemplate(cfg, "redpanda.yaml.ftl", root); return Transferable.of(file, 0700); } @SneakyThrows private String resolveTemplate(Configuration cfg, String template, Map data) { Template temp = cfg.getTemplate(template); @Cleanup ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); @Cleanup Writer out = new OutputStreamWriter(byteArrayOutputStream, StandardCharsets.UTF_8); temp.process(data, out); return new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8); } @Data @AllArgsConstructor private static class Listener { private String address; private int port; } } ================================================ FILE: modules/redpanda/src/main/resources/testcontainers/bootstrap.yaml.ftl ================================================ # Injected by testcontainers # This file contains cluster properties which will only be considered when # starting the cluster for the first time. Afterwards, you can configure cluster # properties via the Redpanda Admin API. superusers: <#if kafkaApi.superusers?has_content > <#list kafkaApi.superusers as superuser> - ${superuser} <#else> [] <#if kafkaApi.enableAuthorization > kafka_enable_authorization: true auto_create_topics_enabled: true ================================================ FILE: modules/redpanda/src/main/resources/testcontainers/entrypoint-tc.sh ================================================ #!/usr/bin/env bash # Wait for testcontainer's injected redpanda config with the port only known after docker start until grep -q "# Injected by testcontainers" "/etc/redpanda/redpanda.yaml" do sleep 0.1 done exec /entrypoint.sh $@ ================================================ FILE: modules/redpanda/src/main/resources/testcontainers/redpanda.yaml.ftl ================================================ # Injected by testcontainers <#setting boolean_format="c"> <#setting number_format="c"> redpanda: admin: address: 0.0.0.0 port: 9644 kafka_api: - address: 0.0.0.0 name: external port: 9092 authentication_method: ${ kafkaApi.authenticationMethod } # This listener is required for the schema registry client. The schema # registry client connects via an advertised listener like a normal Kafka # client would do. It can't use the other listener because the mapped # port is not accessible from within the Redpanda container. - address: 0.0.0.0 name: internal port: 9093 authentication_method: <#if kafkaApi.enableAuthorization >sasl<#else>none <#list kafkaApi.listeners as listener> - address: 0.0.0.0 name: ${listener.address} port: ${listener.port} authentication_method: ${listener.authentication_method} <#list kafka.listeners as listener> - address: ${listener.address} name: ${listener.name} port: ${listener.port} authentication_method: ${listener.authentication_method} advertised_kafka_api: - address: ${ kafkaApi.advertisedHost } name: external port: ${ kafkaApi.advertisedPort } - address: 127.0.0.1 name: internal port: 9093 <#list kafkaApi.listeners as listener> - address: ${listener.address} name: ${listener.address} port: ${listener.port} <#list kafka.advertisedListeners as listener> - address: ${listener.address} name: ${listener.name} port: ${listener.port} schema_registry: schema_registry_api: - address: "0.0.0.0" name: main port: 8081 authentication_method: ${ schemaRegistry.authenticationMethod } schema_registry_client: brokers: - address: localhost port: 9093 pandaproxy: pandaproxy_api: - address: 0.0.0.0 port: 8082 name: proxy-internal advertised_pandaproxy_api: - address: 127.0.0.1 port: 8082 name: proxy-internal pandaproxy_client: brokers: - address: localhost port: 9093 rpk: kafka_api: brokers: - localhost:9093 ================================================ FILE: modules/redpanda/src/test/java/org/testcontainers/redpanda/AbstractRedpanda.java ================================================ package org.testcontainers.redpanda; import com.google.common.collect.ImmutableMap; import org.apache.kafka.clients.admin.AdminClient; import org.apache.kafka.clients.admin.AdminClientConfig; import org.apache.kafka.clients.admin.NewTopic; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecords; import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.common.serialization.StringDeserializer; import org.apache.kafka.common.serialization.StringSerializer; import org.awaitility.Awaitility; import java.time.Duration; import java.util.Collection; import java.util.Collections; import java.util.UUID; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; public class AbstractRedpanda { protected void testKafkaFunctionality(String bootstrapServers) throws Exception { testKafkaFunctionality(bootstrapServers, 1, 1); } protected void testKafkaFunctionality(String bootstrapServers, int partitions, int rf) throws Exception { try ( AdminClient adminClient = AdminClient.create( ImmutableMap.of(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers) ); KafkaProducer producer = new KafkaProducer<>( ImmutableMap.of( ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers, ProducerConfig.CLIENT_ID_CONFIG, UUID.randomUUID().toString() ), new StringSerializer(), new StringSerializer() ); KafkaConsumer consumer = new KafkaConsumer<>( ImmutableMap.of( ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers, ConsumerConfig.GROUP_ID_CONFIG, "tc-" + UUID.randomUUID(), ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest" ), new StringDeserializer(), new StringDeserializer() ); ) { String topicName = "messages-" + UUID.randomUUID(); Collection topics = Collections.singletonList(new NewTopic(topicName, partitions, (short) rf)); adminClient.createTopics(topics).all().get(30, TimeUnit.SECONDS); consumer.subscribe(Collections.singletonList(topicName)); producer.send(new ProducerRecord<>(topicName, "testcontainers", "rulezzz")).get(); Awaitility .await() .atMost(Duration.ofSeconds(10)) .untilAsserted(() -> { ConsumerRecords records = consumer.poll(Duration.ofMillis(100)); assertThat(records) .hasSize(1) .extracting(ConsumerRecord::topic, ConsumerRecord::key, ConsumerRecord::value) .containsExactly(tuple(topicName, "testcontainers", "rulezzz")); }); consumer.unsubscribe(); } } } ================================================ FILE: modules/redpanda/src/test/java/org/testcontainers/redpanda/CompatibleImageTest.java ================================================ package org.testcontainers.redpanda; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; class CompatibleImageTest extends AbstractRedpanda { public static String[] image() { return new String[] { "docker.redpanda.com/redpandadata/redpanda:v22.2.1", "redpandadata/redpanda:v22.2.1" }; } @ParameterizedTest @MethodSource("image") void shouldProduceAndConsumeMessage(String image) throws Exception { try (RedpandaContainer container = new RedpandaContainer(image)) { container.start(); testKafkaFunctionality(container.getBootstrapServers()); } } } ================================================ FILE: modules/redpanda/src/test/java/org/testcontainers/redpanda/RedpandaContainerTest.java ================================================ package org.testcontainers.redpanda; import com.google.common.collect.ImmutableMap; import io.restassured.RestAssured; import io.restassured.common.mapper.TypeRef; import io.restassured.response.Response; import lombok.SneakyThrows; import org.apache.kafka.clients.admin.AdminClient; import org.apache.kafka.clients.admin.AdminClientConfig; import org.apache.kafka.clients.admin.NewTopic; import org.apache.kafka.common.config.SaslConfigs; import org.apache.kafka.common.errors.SaslAuthenticationException; import org.apache.kafka.common.errors.TopicAuthorizationException; import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.SocatContainer; import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.DockerImageName; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class RedpandaContainerTest extends AbstractRedpanda { private static final String REDPANDA_IMAGE = "docker.redpanda.com/redpandadata/redpanda:v22.2.1"; private static final DockerImageName REDPANDA_DOCKER_IMAGE = DockerImageName.parse(REDPANDA_IMAGE); @Test void testUsage() throws Exception { try (RedpandaContainer container = new RedpandaContainer(REDPANDA_DOCKER_IMAGE)) { container.start(); testKafkaFunctionality(container.getBootstrapServers()); } } @Test void testUsageWithStringImage() throws Exception { try ( // constructorWithVersion { RedpandaContainer container = new RedpandaContainer("docker.redpanda.com/redpandadata/redpanda:v23.1.2") // } ) { container.start(); testKafkaFunctionality( // getBootstrapServers { container.getBootstrapServers() // } ); } } @Test void testNotCompatibleVersion() { assertThatThrownBy(() -> new RedpandaContainer("docker.redpanda.com/redpandadata/redpanda:v21.11.19")) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Redpanda version must be >= v22.2.1"); } @Test void redpandadataRedpandaImageVersion2221ShouldNotBeCompatible() { assertThatThrownBy(() -> new RedpandaContainer("redpandadata/redpanda:v21.11.19")) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Redpanda version must be >= v22.2.1"); } @Test void testSchemaRegistry() { try (RedpandaContainer container = new RedpandaContainer(REDPANDA_DOCKER_IMAGE)) { container.start(); String subjectsEndpoint = String.format( "%s/subjects", // getSchemaRegistryAddress { container.getSchemaRegistryAddress() // } ); String subjectName = String.format("test-%s-value", UUID.randomUUID()); Response createSubject = RestAssured .given() .contentType("application/vnd.schemaregistry.v1+json") .pathParam("subject", subjectName) .body("{\"schema\": \"{\\\"type\\\": \\\"string\\\"}\"}") .when() .post(subjectsEndpoint + "/{subject}/versions") .thenReturn(); assertThat(createSubject.getStatusCode()).isEqualTo(200); Response allSubjects = RestAssured.given().when().get(subjectsEndpoint).thenReturn(); assertThat(allSubjects.getStatusCode()).isEqualTo(200); assertThat(allSubjects.jsonPath().getList("$")).contains(subjectName); } } @Test void testUsageWithListener() throws Exception { try ( Network network = Network.newNetwork(); RedpandaContainer redpanda = new RedpandaContainer("docker.redpanda.com/redpandadata/redpanda:v23.1.7") .withListener(() -> "redpanda:19092") .withNetwork(network); GenericContainer kcat = new GenericContainer<>("confluentinc/cp-kcat:7.9.0") .withCreateContainerCmdModifier(cmd -> { cmd.withEntrypoint("sh"); }) .withCopyToContainer(Transferable.of("Message produced by kcat"), "/data/msgs.txt") .withNetwork(network) .withCommand("-c", "tail -f /dev/null") ) { redpanda.start(); kcat.start(); kcat.execInContainer("kcat", "-b", "redpanda:19092", "-t", "msgs", "-P", "-l", "/data/msgs.txt"); String stdout = kcat .execInContainer("kcat", "-b", "redpanda:19092", "-C", "-t", "msgs", "-c", "1") .getStdout(); assertThat(stdout).contains("Message produced by kcat"); } } @Test void testUsageWithListenerInTheSameNetwork() throws Exception { try ( Network network = Network.newNetwork(); // registerListener { RedpandaContainer kafka = new RedpandaContainer("docker.redpanda.com/redpandadata/redpanda:v23.1.7") .withListener("kafka:19092") .withNetwork(network); // } // createKCatContainer { GenericContainer kcat = new GenericContainer<>("confluentinc/cp-kcat:7.9.0") .withCreateContainerCmdModifier(cmd -> { cmd.withEntrypoint("sh"); }) .withCopyToContainer(Transferable.of("Message produced by kcat"), "/data/msgs.txt") .withNetwork(network) .withCommand("-c", "tail -f /dev/null") // } ) { kafka.start(); kcat.start(); // produceConsumeMessage { kcat.execInContainer("kcat", "-b", "kafka:19092", "-t", "msgs", "-P", "-l", "/data/msgs.txt"); String stdout = kcat .execInContainer("kcat", "-b", "kafka:19092", "-C", "-t", "msgs", "-c", "1") .getStdout(); // } assertThat(stdout).contains("Message produced by kcat"); } } @Test void testUsageWithListenerFromProxy() throws Exception { try ( Network network = Network.newNetwork(); // createProxy { SocatContainer socat = new SocatContainer().withNetwork(network).withTarget(2000, "kafka", 19092); // } // registerListenerAndAdvertisedListener { RedpandaContainer kafka = new RedpandaContainer("docker.redpanda.com/redpandadata/redpanda:v23.1.7") .withListener("kafka:19092", () -> socat.getHost() + ":" + socat.getMappedPort(2000)) .withNetwork(network) // } ) { socat.start(); kafka.start(); // produceConsumeMessageFromProxy { String bootstrapServers = String.format("%s:%s", socat.getHost(), socat.getMappedPort(2000)); testKafkaFunctionality(bootstrapServers); // } } } @Test void testUsageWithListenerAndSasl() throws Exception { final String username = "panda"; final String password = "pandapass"; final String algorithm = "SCRAM-SHA-256"; try ( Network network = Network.newNetwork(); RedpandaContainer redpanda = new RedpandaContainer("docker.redpanda.com/redpandadata/redpanda:v23.1.7") .enableAuthorization() .enableSasl() .withSuperuser("panda") .withListener("my-panda:29092") .withNetwork(network); GenericContainer kcat = new GenericContainer<>("confluentinc/cp-kcat:7.9.0") .withCreateContainerCmdModifier(cmd -> { cmd.withEntrypoint("sh"); }) .withCopyToContainer(Transferable.of("Message produced by kcat"), "/data/msgs.txt") .withNetwork(network) .withCommand("-c", "tail -f /dev/null") ) { redpanda.start(); String adminUrl = String.format("%s/v1/security/users", redpanda.getAdminAddress()); Map params = new HashMap<>(); params.put("username", username); params.put("password", password); params.put("algorithm", algorithm); RestAssured.given().contentType("application/json").body(params).post(adminUrl).then().statusCode(200); kcat.start(); kcat.execInContainer( "kcat", "-b", "my-panda:29092", "-X", "security.protocol=SASL_PLAINTEXT", "-X", "sasl.mechanisms=" + algorithm, "-X", "sasl.username=" + username, "-X", "sasl.password=" + password, "-t", "msgs", "-P", "-l", "/data/msgs.txt" ); String stdout = kcat .execInContainer( "kcat", "-b", "my-panda:29092", "-X", "security.protocol=SASL_PLAINTEXT", "-X", "sasl.mechanisms=" + algorithm, "-X", "sasl.username=" + username, "-X", "sasl.password=" + password, "-C", "-t", "msgs", "-c", "1" ) .getStdout(); assertThat(stdout).contains("Message produced by kcat"); } } @SneakyThrows @Test void enableSaslWithSuccessfulTopicCreation() { try ( // security { RedpandaContainer redpanda = new RedpandaContainer("docker.redpanda.com/redpandadata/redpanda:v23.1.7") .enableAuthorization() .enableSasl() .withSuperuser("superuser-1") // } ) { redpanda.start(); createSuperUser(redpanda); AdminClient adminClient = getAdminClient(redpanda); String topicName = "messages-" + UUID.randomUUID(); Collection topics = Collections.singletonList(new NewTopic(topicName, 1, (short) 1)); adminClient.createTopics(topics).all().get(30, TimeUnit.SECONDS); assertThat(adminClient.listTopics().names().get()).contains(topicName); } } @Test void enableSaslWithUnsuccessfulTopicCreation() { try ( RedpandaContainer redpanda = new RedpandaContainer("docker.redpanda.com/redpandadata/redpanda:v23.1.7") .enableAuthorization() .enableSasl() ) { redpanda.start(); createSuperUser(redpanda); AdminClient adminClient = getAdminClient(redpanda); String topicName = "messages-" + UUID.randomUUID(); Collection topics = Collections.singletonList(new NewTopic(topicName, 1, (short) 1)); Awaitility .await() .untilAsserted(() -> { assertThatThrownBy(() -> adminClient.createTopics(topics).all().get(30, TimeUnit.SECONDS)) .hasCauseInstanceOf(TopicAuthorizationException.class); }); } } @Test void enableSaslAndWithAuthenticationError() { try ( RedpandaContainer redpanda = new RedpandaContainer("docker.redpanda.com/redpandadata/redpanda:v23.1.7") .enableAuthorization() .enableSasl() ) { redpanda.start(); AdminClient adminClient = getAdminClient(redpanda); String topicName = "messages-" + UUID.randomUUID(); Collection topics = Collections.singletonList(new NewTopic(topicName, 1, (short) 1)); Awaitility .await() .untilAsserted(() -> { assertThatThrownBy(() -> adminClient.createTopics(topics).all().get(30, TimeUnit.SECONDS)) .hasCauseInstanceOf(SaslAuthenticationException.class); }); } } @Test void schemaRegistryWithHttpBasic() { try ( RedpandaContainer redpanda = new RedpandaContainer("docker.redpanda.com/redpandadata/redpanda:v23.1.7") .enableSchemaRegistryHttpBasicAuth() .withSuperuser("superuser-1") ) { redpanda.start(); createSuperUser(redpanda); String subjectsEndpoint = String.format("%s/subjects", redpanda.getSchemaRegistryAddress()); RestAssured.when().get(subjectsEndpoint).then().statusCode(401); RestAssured .given() .auth() .preemptive() .basic("superuser-1", "test") .get(subjectsEndpoint) .then() .statusCode(200); } } @SneakyThrows @Test void testRestProxy() { try (RedpandaContainer redpanda = new RedpandaContainer("docker.redpanda.com/redpandadata/redpanda:v23.1.7")) { redpanda.start(); redpanda.execInContainer("rpk", "topic", "create", "test_topic", "-p", "3"); String applicationKafkaJson = "application/vnd.kafka.json.v2+json"; String restProxy = redpanda.getRestProxyAddress(); RestAssured .given() .contentType(applicationKafkaJson) .body( "{\"records\":[{\"value\":\"jsmith\",\"partition\":0},{\"value\":\"htanaka\",\"partition\":1},{\"value\":\"awalther\",\"partition\":2}]}" ) .post(String.format("%s/topics/test_topic", restProxy)) .then() .statusCode(200); RestAssured .given() .contentType("application/vnd.kafka.v2+json") .body("{\"name\": \"test_consumer\", \"format\": \"json\", \"auto.offset.reset\": \"earliest\"}") .post(String.format("%s/consumers/test_group", restProxy)) .then() .statusCode(200); RestAssured .given() .contentType("application/vnd.kafka.v2+json") .body("{\"topics\":[\"test_topic\"]}") .post(String.format("%s/consumers/test_group/instances/test_consumer/subscription", restProxy)) .then() .statusCode(204); List> response = RestAssured .given() .accept(applicationKafkaJson) .get(String.format("%s/consumers/test_group/instances/test_consumer/records", restProxy)) .getBody() .as(new TypeRef>>() {}); assertThat(response).hasSize(3).extracting("value").containsExactly("jsmith", "htanaka", "awalther"); } } private AdminClient getAdminClient(RedpandaContainer redpanda) { String bootstrapServer = String.format("%s:%s", redpanda.getHost(), redpanda.getMappedPort(9092)); // createAdminClient { AdminClient adminClient = AdminClient.create( ImmutableMap.of( AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServer, AdminClientConfig.SECURITY_PROTOCOL_CONFIG, "SASL_PLAINTEXT", SaslConfigs.SASL_MECHANISM, "SCRAM-SHA-256", SaslConfigs.SASL_JAAS_CONFIG, "org.apache.kafka.common.security.scram.ScramLoginModule required username=\"superuser-1\" password=\"test\";" ) ); // } return adminClient; } private void createSuperUser(RedpandaContainer redpanda) { String adminUrl = String.format("%s/v1/security/users", redpanda.getAdminAddress()); RestAssured .given() .contentType("application/json") .body("{\"username\": \"superuser-1\", \"password\": \"test\", \"algorithm\": \"SCRAM-SHA-256\"}") .post(adminUrl) .then() .statusCode(200); } } ================================================ FILE: modules/redpanda/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/scylladb/build.gradle ================================================ description = "Testcontainers :: ScyllaDB" dependencies { api project(":testcontainers") testImplementation 'com.scylladb:java-driver-core:4.19.0.4' testImplementation 'software.amazon.awssdk:dynamodb:2.40.4' } ================================================ FILE: modules/scylladb/src/main/java/org/testcontainers/scylladb/ScyllaDBContainer.java ================================================ package org.testcontainers.scylladb; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.net.InetSocketAddress; import java.util.Optional; /** * Testcontainers implementation for ScyllaDB. *

* Supported image: {@code scylladb/scylla} *

* Exposed ports: *

    *
  • CQL Port: 9042
  • *
  • Shard Aware Port: 19042
  • *
  • Alternator Port: 8000
  • *
*/ public class ScyllaDBContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("scylladb/scylla"); private static final Integer CQL_PORT = 9042; private static final Integer SHARD_AWARE_PORT = 19042; private static final Integer ALTERNATOR_PORT = 8000; private static final String COMMAND = "--developer-mode=1 --overprovisioned=1"; private static final String CONTAINER_CONFIG_LOCATION = "/etc/scylla"; private boolean alternatorEnabled = false; private String configLocation; public ScyllaDBContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public ScyllaDBContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); withExposedPorts(CQL_PORT, SHARD_AWARE_PORT); withCommand(COMMAND); waitingFor(Wait.forLogMessage(".*initialization completed..*", 1)); } @Override protected void configure() { if (this.alternatorEnabled) { addExposedPort(8000); String newCommand = COMMAND + " --alternator-port=" + ALTERNATOR_PORT + " --alternator-write-isolation=always"; withCommand(newCommand); } // Map (effectively replace) directory in Docker with the content of resourceLocation if resource location is // not null. Optional .ofNullable(configLocation) .map(MountableFile::forClasspathResource) .ifPresent(mountableFile -> withCopyFileToContainer(mountableFile, CONTAINER_CONFIG_LOCATION)); } public ScyllaDBContainer withConfigurationOverride(String configLocation) { this.configLocation = configLocation; return this; } public ScyllaDBContainer withSsl(MountableFile certificate, MountableFile keyfile, MountableFile truststore) { withCopyFileToContainer(certificate, "/etc/scylla/scylla.cer.pem"); withCopyFileToContainer(keyfile, "/etc/scylla/scylla.key.pem"); withCopyFileToContainer(truststore, "/etc/scylla/scylla.truststore"); withEnv("SSL_CERTFILE", "/etc/scylla/scylla.cer.pem"); return this; } public ScyllaDBContainer withAlternator() { this.alternatorEnabled = true; return this; } /** * Retrieve an {@link InetSocketAddress} for connecting to the ScyllaDB container via the driver. * * @return A InetSocketAddress representation of this ScyllaDB container's host and port. */ public InetSocketAddress getContactPoint() { return new InetSocketAddress(getHost(), getMappedPort(CQL_PORT)); } public InetSocketAddress getShardAwareContactPoint() { return new InetSocketAddress(getHost(), getMappedPort(SHARD_AWARE_PORT)); } public String getAlternatorEndpoint() { if (!this.alternatorEnabled) { throw new IllegalStateException("Alternator is not enabled"); } return "http://" + getHost() + ":" + getMappedPort(ALTERNATOR_PORT); } } ================================================ FILE: modules/scylladb/src/test/java/org/testcontainers/scylladb/ScyllaDBContainerTest.java ================================================ package org.testcontainers.scylladb; import com.datastax.oss.driver.api.core.CqlSession; import com.datastax.oss.driver.api.core.cql.ResultSet; import org.junit.jupiter.api.Test; import org.testcontainers.containers.Container; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; import software.amazon.awssdk.services.dynamodb.model.BillingMode; import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; import software.amazon.awssdk.services.dynamodb.model.KeyType; import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; import java.io.IOException; import java.net.URI; import java.nio.file.Files; import java.nio.file.Paths; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class ScyllaDBContainerTest { private static final DockerImageName SCYLLADB_IMAGE = DockerImageName.parse("scylladb/scylla:6.2"); private static final String BASIC_QUERY = "SELECT release_version FROM system.local"; @Test void testSimple() { try ( // container { ScyllaDBContainer scylladb = new ScyllaDBContainer("scylladb/scylla:6.2") // } ) { scylladb.start(); // session { CqlSession session = CqlSession .builder() .addContactPoint(scylladb.getContactPoint()) .withLocalDatacenter("datacenter1") .build(); // } ResultSet resultSet = session.execute(BASIC_QUERY); assertThat(resultSet.wasApplied()).isTrue(); assertThat(resultSet.one().getString(0)).isNotNull(); assertThat(session.getMetadata().getNodes().values()).hasSize(1); } } @Test void testSimpleSsl() throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException, UnrecoverableKeyException, KeyManagementException { try ( // customConfiguration { ScyllaDBContainer scylladb = new ScyllaDBContainer("scylladb/scylla:6.2") .withConfigurationOverride("scylla-test-ssl") .withSsl( MountableFile.forClasspathResource("keys/scylla.cer.pem"), MountableFile.forClasspathResource("keys/scylla.key.pem"), MountableFile.forClasspathResource("keys/scylla.truststore") ) // } ) { // sslContext { String testResourcesDir = getClass().getClassLoader().getResource("keys/").getPath(); KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load( Files.newInputStream(Paths.get(testResourcesDir + "scylla.keystore")), "scylla".toCharArray() ); KeyStore trustStore = KeyStore.getInstance("PKCS12"); trustStore.load( Files.newInputStream(Paths.get(testResourcesDir + "scylla.truststore")), "scylla".toCharArray() ); KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance( KeyManagerFactory.getDefaultAlgorithm() ); keyManagerFactory.init(keyStore, "scylla".toCharArray()); TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm() ); trustManagerFactory.init(trustStore); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); // } scylladb.start(); CqlSession session = CqlSession .builder() .addContactPoint(scylladb.getContactPoint()) .withLocalDatacenter("datacenter1") .withSslContext(sslContext) .build(); ResultSet resultSet = session.execute(BASIC_QUERY); assertThat(resultSet.wasApplied()).isTrue(); assertThat(resultSet.one().getString(0)).isNotNull(); assertThat(session.getMetadata().getNodes().values()).hasSize(1); } } @Test void testSimpleSslCqlsh() throws IllegalStateException, InterruptedException, IOException { try ( ScyllaDBContainer scylladb = new ScyllaDBContainer(SCYLLADB_IMAGE) .withConfigurationOverride("scylla-test-ssl") .withSsl( MountableFile.forClasspathResource("keys/scylla.cer.pem"), MountableFile.forClasspathResource("keys/scylla.key.pem"), MountableFile.forClasspathResource("keys/scylla.truststore") ) ) { scylladb.start(); Container.ExecResult execResult = scylladb.execInContainer( "cqlsh", "--ssl", "-e", "select * from system_schema.keyspaces;" ); assertThat(execResult.getStdout()).contains("keyspace_name"); } } @Test void testShardAwareness() { try (ScyllaDBContainer scylladb = new ScyllaDBContainer(SCYLLADB_IMAGE)) { scylladb.start(); // shardAwarenessSession { CqlSession session = CqlSession .builder() .addContactPoint(scylladb.getShardAwareContactPoint()) .withLocalDatacenter("datacenter1") .build(); // } ResultSet resultSet = session.execute("SELECT driver_name FROM system.clients"); assertThat(resultSet.one().getString(0)).isNotNull(); assertThat(session.getMetadata().getNodes().values()).hasSize(1); } } @Test void testAlternator() { try ( // alternator { ScyllaDBContainer scylladb = new ScyllaDBContainer(SCYLLADB_IMAGE).withAlternator() // } ) { scylladb.start(); // dynamodDbClient { DynamoDbClient client = DynamoDbClient .builder() .endpointOverride(URI.create(scylladb.getAlternatorEndpoint())) .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("test", "test"))) .region(Region.US_EAST_1) .build(); // } client.createTable( CreateTableRequest .builder() .tableName("demo_table") .keySchema(KeySchemaElement.builder().attributeName("id").keyType(KeyType.HASH).build()) .attributeDefinitions( AttributeDefinition.builder().attributeName("id").attributeType(ScalarAttributeType.S).build() ) .billingMode(BillingMode.PAY_PER_REQUEST) .build() ); assertThat(client.listTables().tableNames()).containsExactly(("demo_table")); } } @Test void throwExceptionWhenAlternatorDisabled() { try (ScyllaDBContainer scylladb = new ScyllaDBContainer(SCYLLADB_IMAGE)) { scylladb.start(); assertThatThrownBy(scylladb::getAlternatorEndpoint) .isInstanceOf(IllegalStateException.class) .hasMessageContaining("Alternator is not enabled"); } } } ================================================ FILE: modules/scylladb/src/test/resources/keys/node0.cer ================================================ ================================================ FILE: modules/scylladb/src/test/resources/keys/node0.p12 ================================================ ================================================ FILE: modules/scylladb/src/test/resources/keys/scylla.cer.pem ================================================ Bag Attributes friendlyName: node0 localKeyID: 54 69 6D 65 20 31 37 33 35 39 34 30 37 38 39 31 39 34 subject=C=None, L=None, O=None, OU=None, CN=None issuer=C=None, L=None, O=None, OU=None, CN=None -----BEGIN CERTIFICATE----- MIIEOzCCAqOgAwIBAgIIY4iVNsJSWiEwDQYJKoZIhvcNAQEMBQAwSzENMAsGA1UE BhMETm9uZTENMAsGA1UEBxMETm9uZTENMAsGA1UEChMETm9uZTENMAsGA1UECxME Tm9uZTENMAsGA1UEAxMETm9uZTAgFw0yNTAxMDMyMTM5MzFaGA8yMTI0MTIxMDIx MzkzMVowSzENMAsGA1UEBhMETm9uZTENMAsGA1UEBxMETm9uZTENMAsGA1UEChME Tm9uZTENMAsGA1UECxMETm9uZTENMAsGA1UEAxMETm9uZTCCAaIwDQYJKoZIhvcN AQEBBQADggGPADCCAYoCggGBAJuC18n+jlDcmR8CWxSK3fR2t1Am8P7IK5FY3ky8 vEJSCMh+GoiqXVq67zhpOJnlgvEEZIDJGzBmJ/nIZvQwIAMxs792fHIEpEI2GTpf oaMf/9AAuPXuscg+5i4us1eVyVbrq3sREJ2NXHIPylcjtbwLjuepvmXTLp1d7oOJ Ad0X0W3UN/uwrlV3NPBuVLjJiCvJijWrCv1lFTuIcclqs478ozllp8UfcwJ57OH2 Hq1ee9Ex9y7HouDPfFzmMRp1/jEcb0xbefpdW3Am6P9AXQuw2JMempwt5KbrAE+Z V1JnZCjSYSkspwid2bt5To/o60ypZUUswElasgAV/k8AxxDOkJGZusEqqVH7EFvk h3FiY/jb9cM1t5eLcpjx0wA+GOuErW3dgH5/WYugY2iiYjP1IQTb8Pk+gfAvq+2p SX3wISDCAh53j+aceUvNf+lItXsz66V9e+VH1xcOZcyO4gAMUVNYQFv/2wZ9knK4 o30Aiqir1g2Hd5F/rWYNum+UbQIDAQABoyEwHzAdBgNVHQ4EFgQUqAWcYa3l/OHI JACasy+bZUwHP9kwDQYJKoZIhvcNAQEMBQADggGBAJQo55VJd8aEv6uiC5bKdACo M1GMvxWXUFzTdh2XKTOMF5GWwGJ3WRuW9o9wMZwXjvRihPfnx+DnfCCgZBOTGLXB 3ObsogR9rij4uquUIkGJsshggY2gO82NVD7dRwGClncwTI+/RU7qGUym4SEdg6GP yfad3eTvqscQU1mNTxkaH0IDzPm0SWF8lcgGnrdHWlN+Nb8MJSHL5NFc9DA9pZck 5/4MG1X8Hsk/UT04ln+8VrhYFkxkDv4fSKlr65slrst5721J0j+VLEwnuEl1onpW WHTTTIcOTDR5asrN9ZACCUsBxST8yfoJQ5G4HMO+UI1/1d928Ug6kHNWw2WR5FGG pJVu9vpTdA01MNkSeCuZhaPe2XgZcNPyHXcVxslNvFFZ0FVt6pSIhtmZ+4a8dRsm eU4NQ+PJ24En/8dErxaPqmi31wRZBg5Y9YlugJV4GQszCKHr0OYNK+Lpdq9dboUj 6lxX7+gshUgKMzunUl/rTvddG7e/WuZbi9IvmJ4MYw== -----END CERTIFICATE----- ================================================ FILE: modules/scylladb/src/test/resources/keys/scylla.key.pem ================================================ Bag Attributes friendlyName: node0 localKeyID: 54 69 6D 65 20 31 37 33 35 39 34 30 37 38 39 31 39 34 Key Attributes: -----BEGIN PRIVATE KEY----- MIIG/AIBADANBgkqhkiG9w0BAQEFAASCBuYwggbiAgEAAoIBgQCbgtfJ/o5Q3Jkf AlsUit30drdQJvD+yCuRWN5MvLxCUgjIfhqIql1auu84aTiZ5YLxBGSAyRswZif5 yGb0MCADMbO/dnxyBKRCNhk6X6GjH//QALj17rHIPuYuLrNXlclW66t7ERCdjVxy D8pXI7W8C47nqb5l0y6dXe6DiQHdF9Ft1Df7sK5VdzTwblS4yYgryYo1qwr9ZRU7 iHHJarOO/KM5ZafFH3MCeezh9h6tXnvRMfcux6Lgz3xc5jEadf4xHG9MW3n6XVtw Juj/QF0LsNiTHpqcLeSm6wBPmVdSZ2Qo0mEpLKcIndm7eU6P6OtMqWVFLMBJWrIA Ff5PAMcQzpCRmbrBKqlR+xBb5IdxYmP42/XDNbeXi3KY8dMAPhjrhK1t3YB+f1mL oGNoomIz9SEE2/D5PoHwL6vtqUl98CEgwgIed4/mnHlLzX/pSLV7M+ulfXvlR9cX DmXMjuIADFFTWEBb/9sGfZJyuKN9AIqoq9YNh3eRf61mDbpvlG0CAwEAAQKCAYAT SMt3qhB96I04cjNXPc0+ZoZe8yVJgwscEBgpDfKOitu5+SFTN0UyXiISLcIuG278 cl4ANnAftVtZt0dFGr6thrlSkd/mx7qS12CTg45oyywO4DgPj1UOjvY+Xd4xi0qX c8wlC72yu/ft0RV3bt83fXtwMPWCbQjHzQEp4JCRmUWISBvVI1jLEmhHNHdfHua6 /1gbRaWsPJ/AbTAnGQtBPQUEth1y7W52rSX582pkd2YFUBvl+i2xkSlL3+PQ8zar 5giPYZrGh5pCu/bflAsBGZyRx9keSsRK/bzqE0xeRAwTOir2V6g7LbSKLC04xKNc 06/rHf1gslHNNOC3SjHvPyPfTJFHG9Tm+J5OoGo/Rr/W+GNgFMsFJ1fIq1VedpTt ov4CBnBgew8uHTwCoiL6T7f/ttd206A6nhEZ9tWFf8v0o6+y6Z7g0VniU9IuLRLr hXuKkxbBDZQRO8equlAKtbkqv6YFbGImmF/1YwP1/Ct1TR1BDM3m1UB6eez7BWEC gcEAx2RL8dJCVbKoRMjsKqNNh0R3vIz0+S8PTi3yjFjhggUCWOzlwMVFv/y0ztGf pj6Y41eaIdTwQu76uZra748Uj1Vwj5zAKXhb/THWoAidONFRj+qJ3ylDobrO5Fme RiCFlIfjNc6wYQiGqSMXTF02O67to44G+4zsrz+syIZO3ANOR+uB+LUNqvFKL5Kk BUDtU+r9poIoXkgYylzRb/6H0J+D0fcPGg+LHeRvp3DL6uueDN7eGXxdy7hF/q3L DqHlAoHBAMepUZUe5m6h6wIYWoaXPwvSeuBSHWUiGEqoNCrA/1tBI49AOjfn6ccy vu51ng/hEI/XpQ+QXvM/MNk3wyKe3HMjaPKiRbro9EFtva3pz3SrLoRHHzSGkzW3 iTavg8RKo76Pz7MNEVqfkFn0pYr85EMIe4hmmrdR6nwd1oJY1CEMf4wllhWG+v1y 901xLisuRZFE/X4ASvyDyY0Nh+9Cfd+80QS9fpZwuCR+mHQvIpp89F/Ohqyhk9CU HLncQD2f6QKBwBJZUX/UeJRIV6HU157o3kaXb2ljk1unEAKCyfJOb5o2ecvTKSV/ Qfbz+3OY6Nc0pX8uXZnFbcLLGTmhXYp0IVE7bJtasnhegiCfyH97q3RCFv5md/+Y XYfxl/59nMoZThGoG6mk9qhHT5UbDJbTcR028Nl/RXc6tcE+29isO2+VwktuCczo ZHSZtdkA5qUxH2X8lxEOo0Zh3h4pQoDK7JavR0M4OCSOz5+VmQzQnYNl4WqPy+KO hlcsAwz301rqXQKBwHkY2+9q924gbM4vgTBiqY19EqPdihCd1kfprwJDXl21q2Cm HulrkqILyDwPQFf3NLlZnLZM5Rn5uKH2rTbhTWnUD0IiY9KSmhrY+ZNy3S2w6Zy3 GlkcSkrpT6LIX039y0S4Ksw5X84sOzwkIweijLuPeIVpXetUFrlCy6jxQW/uCaox 3c6euLpiMVZaEBuGjBEo2+rBOLnhIKyZiVn3ZSr/dXK/j/ik0zrnQYYuVHmI0hsN wycPNPzr6GReDuSRiQKBwChoS1Vvv49agWjyViIohGm6GHsY1Y1FNIqddHN5KgfA LGZRm8JhlTBPX89KgWUpemDjRHw84vqF46Md9+eeuovr697/fEVQ1W4FWJs9JLej 2zmRlZqQgFnR6hdeeg1l7V8bPLR1zfl0R7+UkguP1xuI55fZc9H5icMCrOOCo1ug vdBrhNl4Swzn+wTVY62J/GX86Rfeybvn+BJQW4RCuKFqcxqctPuR5i+wMOxKWZP3 fMq1U6czbhYvEjp3Y42Exw== -----END PRIVATE KEY----- ================================================ FILE: modules/scylladb/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/scylladb/src/test/resources/scylla-test-ssl/scylla.yaml ================================================ # Scylla storage config YAML ####################################### # This file is split to two sections: # 1. Supported parameters # 2. Unsupported parameters: reserved for future use or backwards # compatibility. # Scylla will only read and use the first segment ####################################### ### Supported Parameters # The name of the cluster. This is mainly used to prevent machines in # one logical cluster from joining another. # It is recommended to change the default value when creating a new cluster. # You can NOT modify this value for an existing cluster #cluster_name: 'Test Cluster' # This defines the number of tokens randomly assigned to this node on the ring # The more tokens, relative to other nodes, the larger the proportion of data # that this node will store. You probably want all nodes to have the same number # of tokens assuming they have equal hardware capability. num_tokens: 256 # Directory where Scylla should store all its files, which are commitlog, # data, hints, view_hints and saved_caches subdirectories. All of these # subs can be overridden by the respective options below. # If unset, the value defaults to /var/lib/scylla # workdir: /var/lib/scylla # Directory where Scylla should store data on disk. # data_file_directories: # - /var/lib/scylla/data # commit log. when running on magnetic HDD, this should be a # separate spindle than the data directories. # commitlog_directory: /var/lib/scylla/commitlog # schema commit log. A special commitlog instance # used for schema and system tables. # When running on magnetic HDD, this should be a # separate spindle than the data directories. # schema_commitlog_directory: /var/lib/scylla/commitlog/schema # commitlog_sync may be either "periodic" or "batch." # # When in batch mode, Scylla won't ack writes until the commit log # has been fsynced to disk. It will wait # commitlog_sync_batch_window_in_ms milliseconds between fsyncs. # This window should be kept short because the writer threads will # be unable to do extra work while waiting. (You may need to increase # concurrent_writes for the same reason.) # # commitlog_sync: batch # commitlog_sync_batch_window_in_ms: 2 # # the other option is "periodic" where writes may be acked immediately # and the CommitLog is simply synced every commitlog_sync_period_in_ms # milliseconds. commitlog_sync: periodic commitlog_sync_period_in_ms: 10000 # The size of the individual commitlog file segments. A commitlog # segment may be archived, deleted, or recycled once all the data # in it (potentially from each columnfamily in the system) has been # flushed to sstables. # # The default size is 32, which is almost always fine, but if you are # archiving commitlog segments (see commitlog_archiving.properties), # then you probably want a finer granularity of archiving; 8 or 16 MB # is reasonable. commitlog_segment_size_in_mb: 32 # The size of the individual schema commitlog file segments. # # The default size is 128, which is 4 times larger than the default # size of the data commitlog. It's because the segment size puts # a limit on the mutation size that can be written at once, and some # schema mutation writes are much larger than average. schema_commitlog_segment_size_in_mb: 128 # any class that implements the SeedProvider interface and has a # constructor that takes a Map of parameters will do. seed_provider: # Addresses of hosts that are deemed contact points. # Cassandra nodes use this list of hosts to find each other and learn # the topology of the ring. You must change this if you are running # multiple nodes! - class_name: org.apache.cassandra.locator.SimpleSeedProvider parameters: # seeds is actually a comma-delimited list of addresses. # Ex: ",," - seeds: "172.17.0.3,127.0.0.1,172.17.0.2,172.17.0.4,172.17.0.5" # Address to bind to and tell other Scylla nodes to connect to. # You _must_ change this if you want multiple nodes to be able to communicate! # # If you leave broadcast_address (below) empty, then setting listen_address # to 0.0.0.0 is wrong as other nodes will not know how to reach this node. # If you set broadcast_address, then you can set listen_address to 0.0.0.0. listen_address: localhost # Address to broadcast to other Scylla nodes # Leaving this blank will set it to the same value as listen_address # broadcast_address: 1.2.3.4 # When using multiple physical network interfaces, set this to true to listen on broadcast_address # in addition to the listen_address, allowing nodes to communicate in both interfaces. # Ignore this property if the network configuration automatically routes between the public and private networks such as EC2. # # listen_on_broadcast_address: false # port for the CQL native transport to listen for clients on # For security reasons, you should not expose this port to the internet. Firewall it if needed. # To disable the CQL native transport, remove this option and configure native_transport_port_ssl. native_transport_port: 9042 # Like native_transport_port, but clients are forwarded to specific shards, based on the # client-side port numbers. native_shard_aware_transport_port: 19042 # Enabling native transport encryption in client_encryption_options allows you to either use # encryption for the standard port or to use a dedicated, additional port along with the unencrypted # standard native_transport_port. # Enabling client encryption and keeping native_transport_port_ssl disabled will use encryption # for native_transport_port. Setting native_transport_port_ssl to a different value # from native_transport_port will use encryption for native_transport_port_ssl while # keeping native_transport_port unencrypted. #native_transport_port_ssl: 9142 # Like native_transport_port_ssl, but clients are forwarded to specific shards, based on the # client-side port numbers. #native_shard_aware_transport_port_ssl: 19142 # How long the coordinator should wait for read operations to complete read_request_timeout_in_ms: 5000 # How long the coordinator should wait for writes to complete write_request_timeout_in_ms: 2000 # how long a coordinator should continue to retry a CAS operation # that contends with other proposals for the same row cas_contention_timeout_in_ms: 1000 # phi value that must be reached for a host to be marked down. # most users should never need to adjust this. # phi_convict_threshold: 8 # IEndpointSnitch. The snitch has two functions: # - it teaches Scylla enough about your network topology to route # requests efficiently # - it allows Scylla to spread replicas around your cluster to avoid # correlated failures. It does this by grouping machines into # "datacenters" and "racks." Scylla will do its best not to have # more than one replica on the same "rack" (which may not actually # be a physical location) # # IF YOU CHANGE THE SNITCH AFTER DATA IS INSERTED INTO THE CLUSTER, # YOU MUST RUN A FULL REPAIR, SINCE THE SNITCH AFFECTS WHERE REPLICAS # ARE PLACED. # # Out of the box, Scylla provides # - SimpleSnitch: # Treats Strategy order as proximity. This can improve cache # locality when disabling read repair. Only appropriate for # single-datacenter deployments. # - GossipingPropertyFileSnitch # This should be your go-to snitch for production use. The rack # and datacenter for the local node are defined in # cassandra-rackdc.properties and propagated to other nodes via # gossip. If cassandra-topology.properties exists, it is used as a # fallback, allowing migration from the PropertyFileSnitch. # - PropertyFileSnitch: # Proximity is determined by rack and data center, which are # explicitly configured in cassandra-topology.properties. # - Ec2Snitch: # Appropriate for EC2 deployments in a single Region. Loads Region # and Availability Zone information from the EC2 API. The Region is # treated as the datacenter, and the Availability Zone as the rack. # Only private IPs are used, so this will not work across multiple # Regions. # - Ec2MultiRegionSnitch: # Uses public IPs as broadcast_address to allow cross-region # connectivity. (Thus, you should set seed addresses to the public # IP as well.) You will need to open the storage_port or # ssl_storage_port on the public IP firewall. (For intra-Region # traffic, Scylla will switch to the private IP after # establishing a connection.) # - RackInferringSnitch: # Proximity is determined by rack and data center, which are # assumed to correspond to the 3rd and 2nd octet of each node's IP # address, respectively. Unless this happens to match your # deployment conventions, this is best used as an example of # writing a custom Snitch class and is provided in that spirit. # # You can use a custom Snitch by setting this to the full class name # of the snitch, which will be assumed to be on your classpath. endpoint_snitch: SimpleSnitch # The address or interface to bind the native transport server to. # # Set rpc_address OR rpc_interface, not both. Interfaces must correspond # to a single address, IP aliasing is not supported. # # Leaving rpc_address blank has the same effect as on listen_address # (i.e. it will be based on the configured hostname of the node). # # Note that unlike listen_address, you can specify 0.0.0.0, but you must also # set broadcast_rpc_address to a value other than 0.0.0.0. # # For security reasons, you should not expose this port to the internet. Firewall it if needed. # # If you choose to specify the interface by name and the interface has an ipv4 and an ipv6 address # you can specify which should be chosen using rpc_interface_prefer_ipv6. If false the first ipv4 # address will be used. If true the first ipv6 address will be used. Defaults to false preferring # ipv4. If there is only one address it will be selected regardless of ipv4/ipv6. rpc_address: localhost # rpc_interface: eth1 # rpc_interface_prefer_ipv6: false # port for REST API server api_port: 10000 # IP for the REST API server api_address: 127.0.0.1 # Log WARN on any batch size exceeding this value. 128 kiB per batch by default. # Caution should be taken on increasing the size of this threshold as it can lead to node instability. batch_size_warn_threshold_in_kb: 128 # Fail any multiple-partition batch exceeding this value. 1 MiB (8x warn threshold) by default. batch_size_fail_threshold_in_kb: 1024 # Authentication backend, identifying users # Out of the box, Scylla provides org.apache.cassandra.auth.{AllowAllAuthenticator, # PasswordAuthenticator}. # # - AllowAllAuthenticator performs no checks - set it to disable authentication. # - PasswordAuthenticator relies on username/password pairs to authenticate # users. It keeps usernames and hashed passwords in system_auth.credentials table. # Please increase system_auth keyspace replication factor if you use this authenticator. # - com.scylladb.auth.TransitionalAuthenticator requires username/password pair # to authenticate in the same manner as PasswordAuthenticator, but improper credentials # result in being logged in as an anonymous user. Use for upgrading clusters' auth. # authenticator: AllowAllAuthenticator # Authorization backend, implementing IAuthorizer; used to limit access/provide permissions # Out of the box, Scylla provides org.apache.cassandra.auth.{AllowAllAuthorizer, # CassandraAuthorizer}. # # - AllowAllAuthorizer allows any action to any user - set it to disable authorization. # - CassandraAuthorizer stores permissions in system_auth.permissions table. Please # increase system_auth keyspace replication factor if you use this authorizer. # - com.scylladb.auth.TransitionalAuthorizer wraps around the CassandraAuthorizer, using it for # authorizing permission management. Otherwise, it allows all. Use for upgrading # clusters' auth. # authorizer: AllowAllAuthorizer # initial_token allows you to specify tokens manually. While you can use # it with # vnodes (num_tokens > 1, above) -- in which case you should provide a # comma-separated list -- it's primarily used when adding nodes # to legacy clusters # that do not have vnodes enabled. # initial_token: # RPC address to broadcast to drivers and other Scylla nodes. This cannot # be set to 0.0.0.0. If left blank, this will be set to the value of # rpc_address. If rpc_address is set to 0.0.0.0, broadcast_rpc_address must # be set. # broadcast_rpc_address: 1.2.3.4 # Uncomment to enable experimental features # experimental_features: # - udf # - alternator-streams # - broadcast-tables # - keyspace-storage-options # The directory where hints files are stored if hinted handoff is enabled. # hints_directory: /var/lib/scylla/hints # The directory where hints files are stored for materialized-view updates # view_hints_directory: /var/lib/scylla/view_hints # See https://docs.scylladb.com/architecture/anti-entropy/hinted-handoff # May either be "true" or "false" to enable globally, or contain a list # of data centers to enable per-datacenter. # hinted_handoff_enabled: DC1,DC2 # hinted_handoff_enabled: true # this defines the maximum amount of time a dead host will have hints # generated. After it has been dead this long, new hints for it will not be # created until it has been seen alive and gone down again. # max_hint_window_in_ms: 10800000 # 3 hours # Validity period for permissions cache (fetching permissions can be an # expensive operation depending on the authorizer, CassandraAuthorizer is # one example). Defaults to 10000, set to 0 to disable. # Will be disabled automatically for AllowAllAuthorizer. # permissions_validity_in_ms: 10000 # Refresh interval for permissions cache (if enabled). # After this interval, cache entries become eligible for refresh. Upon next # access, an async reload is scheduled and the old value returned until it # completes. If permissions_validity_in_ms is non-zero, then this also must have # a non-zero value. Defaults to 2000. It's recommended to set this value to # be at least 3 times smaller than the permissions_validity_in_ms. # permissions_update_interval_in_ms: 2000 # The partitioner is responsible for distributing groups of rows (by # partition key) across nodes in the cluster. You should leave this # alone for new clusters. The partitioner can NOT be changed without # reloading all data, so when upgrading you should set this to the # same partitioner you were already using. # # Murmur3Partitioner is currently the only supported partitioner, # partitioner: org.apache.cassandra.dht.Murmur3Partitioner # Total space to use for commitlogs. # # If space gets above this value (it will round up to the next nearest # segment multiple), Scylla will flush every dirty CF in the oldest # segment and remove it. So a small total commitlog space will tend # to cause more flush activity on less-active columnfamilies. # # A value of -1 (default) will automatically equate it to the total amount of memory # available for Scylla. commitlog_total_space_in_mb: -1 # TCP port, for commands and data # For security reasons, you should not expose this port to the internet. Firewall it if needed. # storage_port: 7000 # SSL port, for encrypted communication. Unused unless enabled in # encryption_options # For security reasons, you should not expose this port to the internet. Firewall it if needed. # ssl_storage_port: 7001 # listen_interface: eth0 # listen_interface_prefer_ipv6: false # Whether to start the native transport server. # Please note that the address on which the native transport is bound is the # same as the rpc_address. The port however is different and specified below. # start_native_transport: true # The maximum size of allowed frame. Frame (requests) larger than this will # be rejected as invalid. The default is 256MB. # native_transport_max_frame_size_in_mb: 256 # enable or disable keepalive on rpc/native connections # rpc_keepalive: true # Set to true to have Scylla create a hard link to each sstable # flushed or streamed locally in a backups/ subdirectory of the # keyspace data. Removing these links is the operator's # responsibility. # incremental_backups: false # Whether or not to take a snapshot before each compaction. Be # careful using this option, since Scylla won't clean up the # snapshots for you. Mostly useful if you're paranoid when there # is a data format change. # snapshot_before_compaction: false # Whether or not a snapshot is taken of the data before keyspace truncation # or dropping of column families. The STRONGLY advised default of true # should be used to provide data safety. If you set this flag to false, you will # lose data on truncation or drop. # auto_snapshot: true # When executing a scan, within or across a partition, we need to keep the # tombstones seen in memory so we can return them to the coordinator, which # will use them to make sure other replicas also know about the deleted rows. # With workloads that generate a lot of tombstones, this can cause performance # problems and even exhaust the server heap. # (http://www.datastax.com/dev/blog/cassandra-anti-patterns-queues-and-queue-like-datasets) # Adjust the thresholds here if you understand the dangers and want to # scan more tombstones anyway. These thresholds may also be adjusted at runtime # using the StorageService mbean. # tombstone_warn_threshold: 1000 # tombstone_failure_threshold: 100000 # Granularity of the collation index of rows within a partition. # Increase if your rows are large, or if you have a very large # number of rows per partition. The competing goals are these: # 1) a smaller granularity means more index entries are generated # and looking up rows within the partition by collation column # is faster # 2) but, Scylla will keep the collation index in memory for hot # rows (as part of the key cache), so a larger granularity means # you can cache more hot rows # column_index_size_in_kb: 64 # Auto-scaling of the promoted index prevents running out of memory # when the promoted index grows too large (due to partitions with many rows # vs. too small column_index_size_in_kb). When the serialized representation # of the promoted index grows by this threshold, the desired block size # for this partition (initialized to column_index_size_in_kb) # is doubled, to decrease the sampling resolution by half. # # To disable promoted index auto-scaling, set the threshold to 0. # column_index_auto_scale_threshold_in_kb: 10240 # Log a warning when writing partitions larger than this value # compaction_large_partition_warning_threshold_mb: 1000 # Log a warning when writing rows larger than this value # compaction_large_row_warning_threshold_mb: 10 # Log a warning when writing cells larger than this value # compaction_large_cell_warning_threshold_mb: 1 # Log a warning when row number is larger than this value # compaction_rows_count_warning_threshold: 100000 # Log a warning when writing a collection containing more elements than this value # compaction_collection_elements_count_warning_threshold: 10000 # How long the coordinator should wait for seq or index scans to complete # range_request_timeout_in_ms: 10000 # How long the coordinator should wait for writes to complete # counter_write_request_timeout_in_ms: 5000 # How long a coordinator should continue to retry a CAS operation # that contends with other proposals for the same row # cas_contention_timeout_in_ms: 1000 # How long the coordinator should wait for truncates to complete # (This can be much longer, because unless auto_snapshot is disabled # we need to flush first so we can snapshot before removing the data.) # truncate_request_timeout_in_ms: 60000 # The default timeout for other, miscellaneous operations # request_timeout_in_ms: 10000 # Enable or disable inter-node encryption. # You must also generate keys and provide the appropriate key and trust store locations and passwords. # # The available internode options are : all, none, dc, rack # If set to dc scylla will encrypt the traffic between the DCs # If set to rack scylla will encrypt the traffic between the racks # # SSL/TLS algorithm and ciphers used can be controlled by # the priority_string parameter. Info on priority string # syntax and values is available at: # https://gnutls.org/manual/html_node/Priority-Strings.html # # The require_client_auth parameter allows you to # restrict access to service based on certificate # validation. Client must provide a certificate # accepted by the used trust store to connect. # # server_encryption_options: # internode_encryption: none # certificate: conf/scylla.crt # keyfile: conf/scylla.key # truststore: # certficate_revocation_list: # require_client_auth: False # priority_string: # enable or disable client/server encryption. client_encryption_options: enabled: true certificate: /etc/scylla/scylla.cer.pem keyfile: /etc/scylla/scylla.key.pem truststore: /etc/scylla/scylla.truststore truststore_password: scylla # certficate_revocation_list: # require_client_auth: False # priority_string: # internode_compression controls whether traffic between nodes is # compressed. # can be: all - all traffic is compressed # dc - traffic between different datacenters is compressed # none - nothing is compressed. # internode_compression: none # Enables inter-node traffic compression metrics (`scylla_rpc_compression_...`) # and enables a new implementation of inter-node traffic compressors, # capable of using zstd (in addition to the default lz4) # and shared dictionaries. # (Those features must still be enabled by other settings). # Has minor CPU cost. # # internode_compression_enable_advanced: false # Enables training of shared compression dictionaries on inter-node traffic. # New dictionaries are distributed throughout the cluster via Raft, # and used to improve the effectiveness of inter-node traffic compression # when `internode_compression_enable_advanced` is enabled. # # WARNING: this may leak unencrypted data to disk. The trained dictionaries # contain randomly-selected pieces of data written to the cluster. # When the Raft log is unencrypted, those pieces of data will be # written to disk unencrypted. At the moment of writing, there is no # way to encrypt the Raft log. # This problem is tracked by https://github.com/scylladb/scylla-enterprise/issues/4717. # # Can be: never - Dictionaries aren't trained by this node. # when_leader - New dictionaries are trained by this node only if # it's the current Raft leader. # always - Dictionaries are trained by this node unconditionally. # # For efficiency reasons, training shouldn't be enabled on more than one node. # To enable it on a single node, one can let the cluster pick the trainer # by setting `when_leader` on all nodes, or specify one manually by setting `always` # on one node and `never` on others. # # rpc_dict_training_when: never # A number in range [0.0, 1.0] specifying the share of CPU which can be spent # by this node on compressing inter-node traffic with zstd. # # Depending on the workload, enabling zstd might have a drastic negative # effect on performance, so it shouldn't be done lightly. # # internode_compression_zstd_max_cpu_fraction: 0.0 # Enable or disable tcp_nodelay for inter-dc communication. # Disabling it will result in larger (but fewer) network packets being sent, # reducing overhead from the TCP protocol itself, at the cost of increasing # latency if you block for cross-datacenter responses. # inter_dc_tcp_nodelay: false # Relaxation of environment checks. # # Scylla places certain requirements on its environment. If these requirements are # not met, performance and reliability can be degraded. # # These requirements include: # - A filesystem with good support for asynchronous I/O (AIO). Currently, # this means XFS. # # false: strict environment checks are in place; do not start if they are not met. # true: relaxed environment checks; performance and reliability may degraade. # # developer_mode: false # Idle-time background processing # # Scylla can perform certain jobs in the background while the system is otherwise idle, # freeing processor resources when there is other work to be done. # # defragment_memory_on_idle: true # # prometheus port # By default, Scylla opens prometheus API port on port 9180 # setting the port to 0 will disable the prometheus API. # prometheus_port: 9180 # # prometheus address # Leaving this blank will set it to the same value as listen_address. # This means that by default, Scylla listens to the prometheus API on the same # listening address (and therefore network interface) used to listen for # internal communication. If the monitoring node is not in this internal # network, you can override prometheus_address explicitly - e.g., setting # it to 0.0.0.0 to listen on all interfaces. # prometheus_address: 1.2.3.4 # Distribution of data among cores (shards) within a node # # Scylla distributes data within a node among shards, using a round-robin # strategy: # [shard0] [shard1] ... [shardN-1] [shard0] [shard1] ... [shardN-1] ... # # Scylla versions 1.6 and below used just one repetition of the pattern; # this interfered with data placement among nodes (vnodes). # # Scylla versions 1.7 and above use 4096 repetitions of the pattern; this # provides for better data distribution. # # the value below is log (base 2) of the number of repetitions. # # Set to 0 to avoid rewriting all data when upgrading from Scylla 1.6 and # below. # # Keep at 12 for new clusters. murmur3_partitioner_ignore_msb_bits: 12 # Use on a new, parallel algorithm for performing aggregate queries. # Set to `false` to fall-back to the old algorithm. # enable_parallelized_aggregation: true # Time for which task manager task started internally is kept in memory after it completes. # task_ttl_in_seconds: 0 # Time for which task manager task started by user is kept in memory after it completes. # user_task_ttl_in_seconds: 3600 # In materialized views, restrictions are allowed only on the view's primary key columns. # In old versions Scylla mistakenly allowed IS NOT NULL restrictions on columns which were not part # of the view's primary key. These invalid restrictions were ignored. # This option controls the behavior when someone tries to create a view with such invalid IS NOT NULL restrictions. # # Can be true, false, or warn. # * `true`: IS NOT NULL is allowed only on the view's primary key columns, # trying to use it on other columns will cause an error, as it should. # * `false`: Scylla accepts IS NOT NULL restrictions on regular columns, but they're silently ignored. # It's useful for backwards compatibility. # * `warn`: The same as false, but there's a warning about invalid view restrictions. # # To preserve backwards compatibility on old clusters, Scylla's default setting is `warn`. # New clusters have this option set to `true` by scylla.yaml (which overrides the default `warn`) # to make sure that trying to create an invalid view causes an error. strict_is_not_null_in_views: true # The Unix Domain Socket the node uses for maintenance socket. # The possible options are: # * ignore: the node will not open the maintenance socket, # * workdir: the node will open the maintenance socket on the path /cql.m, # where is a path defined by the workdir configuration option, # * : the node will open the maintenance socket on the path . maintenance_socket: ignore # If set to true, configuration parameters defined with LiveUpdate option can be updated in runtime with CQL # by updating system.config virtual table. If we don't want any configuration parameter to be changed in runtime # via CQL, this option should be set to false. This parameter doesn't impose any limits on other mechanisms updating # configuration parameters in runtime, e.g. sending SIGHUP or using API. This option should be set to false # e.g. for cloud users, for whom scylla's configuration should be changed only by support engineers. # live_updatable_config_params_changeable_via_cql: true # **************** # * GUARDRAILS * # **************** # Guardrails to warn or fail when Replication Factor is smaller/greater than the threshold. # Please note that the value of 0 is always allowed, # which means that having no replication at all, i.e. RF = 0, is always valid. # A guardrail value smaller than 0, e.g. -1, means that the guardrail is disabled. # Commenting out a guardrail also means it is disabled. # minimum_replication_factor_fail_threshold: -1 # minimum_replication_factor_warn_threshold: 3 # maximum_replication_factor_warn_threshold: -1 # maximum_replication_factor_fail_threshold: -1 # Guardrails to warn about or disallow creating a keyspace with specific replication strategy. # Each of these 2 settings is a list storing replication strategies considered harmful. # The replication strategies to choose from are: # 1) SimpleStrategy, # 2) NetworkTopologyStrategy, # 3) LocalStrategy, # 4) EverywhereStrategy # # replication_strategy_warn_list: # - SimpleStrategy # replication_strategy_fail_list: # Enable tablets for new keyspaces. # When enabled, newly created keyspaces will have tablets enabled by default. # That can be explicitly disabled in the CREATE KEYSPACE query # by using the `tablets = {'enabled': false}` replication option. # # Correspondingly, when disabled, newly created keyspaces will use vnodes # unless tablets are explicitly enabled in the CREATE KEYSPACE query # by using the `tablets = {'enabled': true}` replication option. # # Note that creating keyspaces with tablets enabled or disabled is irreversible. # The `tablets` option cannot be changed using `ALTER KEYSPACE`. enable_tablets: true ================================================ FILE: modules/selenium/build.gradle ================================================ description = "Testcontainers :: Selenium" dependencies { api project(':testcontainers') provided 'org.seleniumhq.selenium:selenium-remote-driver:4.10.0' provided 'org.seleniumhq.selenium:selenium-chrome-driver:4.10.0' testImplementation platform('org.seleniumhq.selenium:selenium-bom:4.13.0') testImplementation 'org.seleniumhq.selenium:selenium-firefox-driver' testImplementation 'org.seleniumhq.selenium:selenium-edge-driver' testImplementation 'org.seleniumhq.selenium:selenium-support' testImplementation 'org.mortbay.jetty:jetty:6.1.26' testImplementation project(':testcontainers-nginx') compileOnly 'org.jetbrains:annotations:26.0.2-1' } ================================================ FILE: modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java ================================================ package org.testcontainers.containers; import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.api.model.AccessMode; import com.github.dockerjava.api.model.Bind; import com.github.dockerjava.api.model.Volume; import com.google.common.collect.ImmutableSet; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.SystemUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.openqa.selenium.Capabilities; import org.openqa.selenium.chrome.ChromeOptions; import org.openqa.selenium.remote.DesiredCapabilities; import org.openqa.selenium.remote.RemoteWebDriver; import org.rnorth.ducttape.timeouts.Timeouts; import org.rnorth.ducttape.unreliables.Unreliables; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.containers.VncRecordingContainer.VncRecordingFormat; import org.testcontainers.containers.traits.LinkableContainer; import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy; import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; import org.testcontainers.containers.wait.strategy.WaitAllStrategy; import org.testcontainers.containers.wait.strategy.WaitStrategy; import org.testcontainers.lifecycle.TestDescription; import org.testcontainers.lifecycle.TestLifecycleAware; import org.testcontainers.utility.ComparableVersion; import org.testcontainers.utility.DockerImageName; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.nio.file.Files; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; /** * A chrome/firefox/custom container based on SeleniumHQ's standalone container sets. *

* Supported images: {@code selenium/standalone-chrome}, {@code selenium/standalone-firefox}, * {@code selenium/standalone-edge}, {@code selenium/standalone-chrome-debug}, {@code selenium/standalone-firefox-debug} *

* Exposed ports: 4444 * * @deprecated use {@link org.testcontainers.selenium.BrowserWebDriverContainer} instead. */ @Deprecated public class BrowserWebDriverContainer> extends GenericContainer implements LinkableContainer, TestLifecycleAware { private static final DockerImageName CHROME_IMAGE = DockerImageName.parse("selenium/standalone-chrome"); private static final DockerImageName FIREFOX_IMAGE = DockerImageName.parse("selenium/standalone-firefox"); private static final DockerImageName EDGE_IMAGE = DockerImageName.parse("selenium/standalone-edge"); private static final DockerImageName CHROME_DEBUG_IMAGE = DockerImageName.parse("selenium/standalone-chrome-debug"); private static final DockerImageName FIREFOX_DEBUG_IMAGE = DockerImageName.parse( "selenium/standalone-firefox-debug" ); private static final DockerImageName[] COMPATIBLE_IMAGES = new DockerImageName[] { CHROME_IMAGE, FIREFOX_IMAGE, EDGE_IMAGE, CHROME_DEBUG_IMAGE, FIREFOX_DEBUG_IMAGE, }; private static final String DEFAULT_PASSWORD = "secret"; private static final int SELENIUM_PORT = 4444; private static final int VNC_PORT = 5900; private static final String NO_PROXY_KEY = "no_proxy"; private static final String TC_TEMP_DIR_PREFIX = "tc"; @Nullable private Capabilities capabilities; private DockerImageName customImageName = null; @Nullable private RemoteWebDriver driver; private VncRecordingMode recordingMode = VncRecordingMode.RECORD_FAILING; private VncRecordingFormat recordingFormat; private RecordingFileFactory recordingFileFactory; private File vncRecordingDirectory; private VncRecordingContainer vncRecordingContainer = null; private static final Logger LOGGER = LoggerFactory.getLogger(BrowserWebDriverContainer.class); public BrowserWebDriverContainer() { super(); this.waitStrategy = getDefaultWaitStrategy(); this.withRecordingFileFactory(new DefaultRecordingFileFactory()); } /** * Constructor taking a specific webdriver container name and tag * @param dockerImageName Name of the selenium docker image */ public BrowserWebDriverContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } /** * Constructor taking a specific webdriver container name and tag * @param dockerImageName Name of the selenium docker image */ public BrowserWebDriverContainer(DockerImageName dockerImageName) { super(dockerImageName); // we assert compatibility with the chrome/firefox/edge image later, after capabilities are processed this.waitStrategy = getDefaultWaitStrategy(); this.withRecordingFileFactory(new DefaultRecordingFileFactory()); this.customImageName = dockerImageName; // We have to force SKIP mode for the recording by default because we don't know if the image has VNC or not recordingMode = VncRecordingMode.SKIP; } public SELF withCapabilities(Capabilities capabilities) { this.capabilities = capabilities; return self(); } /** * @deprecated Use withCapabilities(Capabilities capabilities) instead: * withCapabilities(new FirefoxOptions()) * * @param capabilities DesiredCapabilities * @return SELF * */ @Deprecated public SELF withDesiredCapabilities(DesiredCapabilities capabilities) { this.capabilities = capabilities; return self(); } @NotNull @Override protected Set getLivenessCheckPorts() { Integer seleniumPort = getMappedPort(SELENIUM_PORT); if (recordingMode == VncRecordingMode.SKIP) { return ImmutableSet.of(seleniumPort); } else { return ImmutableSet.of(seleniumPort, getMappedPort(VNC_PORT)); } } @Override protected void configure() { String seleniumVersion = SeleniumUtils.determineClasspathSeleniumVersion(); if (recordingMode != VncRecordingMode.SKIP) { if (vncRecordingDirectory == null) { try { vncRecordingDirectory = Files.createTempDirectory(TC_TEMP_DIR_PREFIX).toFile(); } catch (IOException e) { // should never happen as per javadoc, since we use valid prefix logger().error("Exception while trying to create temp directory", e); throw new ContainerLaunchException("Exception while trying to create temp directory", e); } } if (getNetwork() == null) { withNetwork(Network.SHARED); } vncRecordingContainer = new VncRecordingContainer(this) .withVncPassword(DEFAULT_PASSWORD) .withVncPort(VNC_PORT) .withVideoFormat(recordingFormat); } if (customImageName != null) { customImageName.assertCompatibleWith(COMPATIBLE_IMAGES); super.setDockerImageName(customImageName.asCanonicalNameString()); } else { DockerImageName standardImageForCapabilities = getStandardImageForCapabilities( capabilities, seleniumVersion ); super.setDockerImageName(standardImageForCapabilities.asCanonicalNameString()); } String timeZone = System.getProperty("user.timezone"); if (timeZone == null || timeZone.isEmpty()) { timeZone = "Etc/UTC"; } addExposedPorts(SELENIUM_PORT, VNC_PORT); addEnv("TZ", timeZone); if (!getEnvMap().containsKey(NO_PROXY_KEY)) { addEnv(NO_PROXY_KEY, "localhost"); } setCommand("/opt/bin/entry_point.sh"); if (getShmSize() == null) { if (SystemUtils.IS_OS_WINDOWS) { withSharedMemorySize(512 * FileUtils.ONE_MB); } else { this.getBinds().add(new Bind("/dev/shm", new Volume("/dev/shm"), AccessMode.rw)); } } /* * Some unreliability of the selenium browser containers has been observed, so allow multiple attempts to start. */ setStartupAttempts(3); } /** * @param capabilities a {@link Capabilities} object for either Chrome or Firefox * @param seleniumVersion the version of selenium in use * @return an image name for the default standalone Docker image for the appropriate browser * * @deprecated note that this method is deprecated and may be removed in the future. The no-args * {@link BrowserWebDriverContainer#BrowserWebDriverContainer()} combined with the * {@link BrowserWebDriverContainer#withCapabilities(Capabilities)} method should be considered. A decision on * removal of this deprecated method will be taken at a future date. */ @Deprecated public static String getDockerImageForCapabilities(Capabilities capabilities, String seleniumVersion) { return getStandardImageForCapabilities(capabilities, seleniumVersion).asCanonicalNameString(); } private static DockerImageName getStandardImageForCapabilities(Capabilities capabilities, String seleniumVersion) { String browserName = capabilities == null ? BrowserType.CHROME : capabilities.getBrowserName(); boolean supportsVncWithoutDebugImage = new ComparableVersion(seleniumVersion).isGreaterThanOrEqualTo("4"); switch (browserName) { case BrowserType.CHROME: return (supportsVncWithoutDebugImage ? CHROME_IMAGE : CHROME_DEBUG_IMAGE).withTag(seleniumVersion); case BrowserType.FIREFOX: return (supportsVncWithoutDebugImage ? FIREFOX_IMAGE : FIREFOX_DEBUG_IMAGE).withTag(seleniumVersion); case BrowserType.EDGE: if (supportsVncWithoutDebugImage) { return EDGE_IMAGE.withTag(seleniumVersion); } throw new UnsupportedOperationException( "For browser 'MicrosoftEdge' selenium version must be 4 or higher;" + "docker images are available from there upwards;" + "provided version: '" + seleniumVersion + "'" ); default: throw new UnsupportedOperationException( "Browser name must be 'chrome', 'firefox' or 'MicrosoftEdge';" + "provided '" + browserName + "' is not supported" ); } } public URL getSeleniumAddress() { try { return new URL("http", getHost(), getMappedPort(SELENIUM_PORT), "/wd/hub"); } catch (MalformedURLException e) { e.printStackTrace(); // TODO return null; } } public String getVncAddress() { return "vnc://vnc:secret@" + getHost() + ":" + getMappedPort(VNC_PORT); } @Deprecated public String getPassword() { return DEFAULT_PASSWORD; } @Deprecated public int getPort() { return VNC_PORT; } @Override protected void containerIsStarted(InspectContainerResponse containerInfo) { if (vncRecordingContainer != null) { LOGGER.debug("Starting VNC recording"); vncRecordingContainer.start(); } } /** * Obtain a RemoteWebDriver instance that is bound to an instance of the browser running inside a new container. *

* All containers and drivers will be automatically shut down after the test method finishes (if used as a @Rule) or the test * class (if used as a @ClassRule) * * @return a new Remote Web Driver instance * @deprecated use {@link #getSeleniumAddress()} instead */ @Deprecated public synchronized RemoteWebDriver getWebDriver() { if (driver == null) { if (capabilities == null) { logger() .warn( "No capabilities provided - this will cause an exception in future versions. Falling back to ChromeOptions" ); capabilities = new ChromeOptions(); } driver = Unreliables.retryUntilSuccess( 30, TimeUnit.SECONDS, () -> { return Timeouts.getWithTimeout( 10, TimeUnit.SECONDS, () -> { return new RemoteWebDriver(getSeleniumAddress(), capabilities); } ); } ); } return driver; } @Override public void afterTest(TestDescription description, Optional throwable) { retainRecordingIfNeeded(description.getFilesystemFriendlyName(), !throwable.isPresent()); } @Override public void stop() { if (driver != null) { try { driver.quit(); } catch (Exception e) { LOGGER.debug("Failed to quit the driver", e); } driver = null; } if (vncRecordingContainer != null) { try { vncRecordingContainer.stop(); } catch (Exception e) { LOGGER.debug("Failed to stop vncRecordingContainer", e); } vncRecordingContainer = null; } super.stop(); } private void retainRecordingIfNeeded(String prefix, boolean succeeded) { final boolean shouldRecord; switch (recordingMode) { case RECORD_ALL: shouldRecord = true; break; case RECORD_FAILING: shouldRecord = !succeeded; break; default: shouldRecord = false; break; } if (shouldRecord) { File recordingFile = recordingFileFactory.recordingFileForTest( vncRecordingDirectory, prefix, succeeded, vncRecordingContainer.getVideoFormat() ); LOGGER.info("Screen recordings for test {} will be stored at: {}", prefix, recordingFile); vncRecordingContainer.saveRecordingToFile(recordingFile); } } /** * Remember any other containers this needs to link to. We have to pass these down to the container so that * the other containers will be initialized before linking occurs. * * @param otherContainer the container rule to link to * @param alias the alias (hostname) that this other container should be referred to by * @return this * * @deprecated Links are deprecated (see #465). Please use {@link Network} features instead. */ @Deprecated public SELF withLinkToContainer(LinkableContainer otherContainer, String alias) { addLink(otherContainer, alias); return self(); } public SELF withRecordingMode(VncRecordingMode recordingMode, File vncRecordingDirectory) { return withRecordingMode(recordingMode, vncRecordingDirectory, null); } public SELF withRecordingMode( VncRecordingMode recordingMode, File vncRecordingDirectory, VncRecordingFormat recordingFormat ) { this.recordingMode = recordingMode; this.vncRecordingDirectory = vncRecordingDirectory; this.recordingFormat = recordingFormat; return self(); } public SELF withRecordingFileFactory(RecordingFileFactory recordingFileFactory) { this.recordingFileFactory = recordingFileFactory; return self(); } private WaitStrategy getDefaultWaitStrategy() { final WaitStrategy logWaitStrategy = new LogMessageWaitStrategy() .withRegEx( ".*(RemoteWebDriver instances should connect to|Selenium Server is up and running|Started Selenium Standalone).*\n" ) .withStartupTimeout(Duration.of(60, ChronoUnit.SECONDS)); return new WaitAllStrategy() .withStrategy(logWaitStrategy) .withStrategy(new HostPortWaitStrategy()) .withStartupTimeout(Duration.of(60, ChronoUnit.SECONDS)); } public enum VncRecordingMode { SKIP, RECORD_ALL, RECORD_FAILING, } private static class BrowserType { private static final String CHROME = "chrome"; private static final String FIREFOX = "firefox"; private static final String EDGE = "MicrosoftEdge"; } } ================================================ FILE: modules/selenium/src/main/java/org/testcontainers/containers/DefaultRecordingFileFactory.java ================================================ package org.testcontainers.containers; import java.io.File; import java.text.SimpleDateFormat; import java.util.Date; public class DefaultRecordingFileFactory implements RecordingFileFactory { private static final SimpleDateFormat filenameDateFormat = new SimpleDateFormat("YYYYMMdd-HHmmss"); private static final String PASSED = "PASSED"; private static final String FAILED = "FAILED"; private static final String FILENAME_FORMAT = "%s-%s-%s.%s"; @Override public File recordingFileForTest(File vncRecordingDirectory, String prefix, boolean succeeded) { return recordingFileForTest( vncRecordingDirectory, prefix, succeeded, VncRecordingContainer.DEFAULT_RECORDING_FORMAT ); } @Override public File recordingFileForTest( File vncRecordingDirectory, String prefix, boolean succeeded, VncRecordingContainer.VncRecordingFormat recordingFormat ) { final String resultMarker = succeeded ? PASSED : FAILED; final String fileName = String.format( FILENAME_FORMAT, resultMarker, prefix, filenameDateFormat.format(new Date()), recordingFormat.getFilenameExtension() ); return new File(vncRecordingDirectory, fileName); } } ================================================ FILE: modules/selenium/src/main/java/org/testcontainers/containers/RecordingFileFactory.java ================================================ package org.testcontainers.containers; import org.testcontainers.containers.VncRecordingContainer.VncRecordingFormat; import java.io.File; public interface RecordingFileFactory { default File recordingFileForTest( File vncRecordingDirectory, String prefix, boolean succeeded, VncRecordingFormat recordingFormat ) { return recordingFileForTest(vncRecordingDirectory, prefix, succeeded); } File recordingFileForTest(File vncRecordingDirectory, String prefix, boolean succeeded); } ================================================ FILE: modules/selenium/src/main/java/org/testcontainers/containers/SeleniumUtils.java ================================================ package org.testcontainers.containers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.InputStream; import java.net.URL; import java.util.Enumeration; import java.util.HashSet; import java.util.Set; import java.util.jar.Attributes; import java.util.jar.Manifest; /** * Utility methods for Selenium. */ public final class SeleniumUtils { public static final String DEFAULT_SELENIUM_VERSION = "2.45.0"; private static final Logger LOGGER = LoggerFactory.getLogger(SeleniumUtils.class); private SeleniumUtils() {} /** * Based on the JARs detected on the classpath, determine which version of selenium-api is available. * @return the detected version of Selenium API, or DEFAULT_SELENIUM_VERSION if it could not be determined */ public static String determineClasspathSeleniumVersion() { Set seleniumVersions = new HashSet<>(); try { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); Enumeration manifests = classLoader.getResources("META-INF/MANIFEST.MF"); while (manifests.hasMoreElements()) { URL manifestURL = manifests.nextElement(); try (InputStream is = manifestURL.openStream()) { Manifest manifest = new Manifest(); manifest.read(is); String seleniumVersion = getSeleniumVersionFromManifest(manifest); if (seleniumVersion != null) { seleniumVersions.add(seleniumVersion); LOGGER.info("Selenium API version {} detected on classpath", seleniumVersion); } } } } catch (Exception e) { LOGGER.debug("Failed to determine Selenium-Version from selenium-api JAR Manifest", e); } if (seleniumVersions.size() == 0) { LOGGER.warn( "Failed to determine Selenium version from classpath - will use default version of {}", DEFAULT_SELENIUM_VERSION ); return DEFAULT_SELENIUM_VERSION; } String foundVersion = seleniumVersions.iterator().next(); if (seleniumVersions.size() > 1) { LOGGER.warn( "Multiple versions of Selenium API found on classpath - will select {}, but this may not be reliable", foundVersion ); } return foundVersion; } /** * Read Manifest to get Selenium Version. * @param manifest manifest * @return Selenium Version detected */ public static String getSeleniumVersionFromManifest(Manifest manifest) { String seleniumVersion = null; Attributes buildInfo = manifest.getAttributes("Build-Info"); if (buildInfo != null) { seleniumVersion = buildInfo.getValue("Selenium-Version"); } // Compatibility Selenium > 3.X if (seleniumVersion == null) { Attributes seleniumInfo = manifest.getAttributes("Selenium"); if (seleniumInfo != null) { seleniumVersion = seleniumInfo.getValue("Selenium-Version"); } } return seleniumVersion; } } ================================================ FILE: modules/selenium/src/main/java/org/testcontainers/selenium/BrowserWebDriverContainer.java ================================================ package org.testcontainers.selenium; import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.api.model.AccessMode; import com.github.dockerjava.api.model.Bind; import com.github.dockerjava.api.model.Volume; import com.google.common.collect.ImmutableSet; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.SystemUtils; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.containers.DefaultRecordingFileFactory; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.RecordingFileFactory; import org.testcontainers.containers.VncRecordingContainer; import org.testcontainers.containers.VncRecordingContainer.VncRecordingFormat; import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy; import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; import org.testcontainers.containers.wait.strategy.WaitAllStrategy; import org.testcontainers.containers.wait.strategy.WaitStrategy; import org.testcontainers.lifecycle.TestDescription; import org.testcontainers.lifecycle.TestLifecycleAware; import org.testcontainers.utility.DockerImageName; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.nio.file.Files; import java.time.Duration; import java.util.Optional; import java.util.Set; /** * A chrome/firefox/custom container based on SeleniumHQ's standalone container sets. *

* Supported images: {@code selenium/standalone-chrome}, {@code selenium/standalone-firefox}, * {@code selenium/standalone-edge}, {@code selenium/standalone-chrome-debug}, {@code selenium/standalone-firefox-debug} *

* Exposed ports: 4444 */ public class BrowserWebDriverContainer extends GenericContainer implements TestLifecycleAware { private static final DockerImageName CHROME_IMAGE = DockerImageName.parse("selenium/standalone-chrome"); private static final DockerImageName FIREFOX_IMAGE = DockerImageName.parse("selenium/standalone-firefox"); private static final DockerImageName EDGE_IMAGE = DockerImageName.parse("selenium/standalone-edge"); private static final DockerImageName CHROME_DEBUG_IMAGE = DockerImageName.parse("selenium/standalone-chrome-debug"); private static final DockerImageName FIREFOX_DEBUG_IMAGE = DockerImageName.parse( "selenium/standalone-firefox-debug" ); private static final DockerImageName[] COMPATIBLE_IMAGES = new DockerImageName[] { CHROME_IMAGE, FIREFOX_IMAGE, EDGE_IMAGE, CHROME_DEBUG_IMAGE, FIREFOX_DEBUG_IMAGE, }; private static final String DEFAULT_PASSWORD = "secret"; private static final int SELENIUM_PORT = 4444; private static final int VNC_PORT = 5900; private static final String NO_PROXY_KEY = "no_proxy"; private static final String TC_TEMP_DIR_PREFIX = "tc"; private VncRecordingMode recordingMode = VncRecordingMode.RECORD_FAILING; private VncRecordingFormat recordingFormat; private RecordingFileFactory recordingFileFactory; private File vncRecordingDirectory; private VncRecordingContainer vncRecordingContainer = null; private static final Logger LOGGER = LoggerFactory.getLogger(BrowserWebDriverContainer.class); /** * Constructor taking a specific webdriver container name and tag * @param dockerImageName Name of the selenium docker image */ public BrowserWebDriverContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } /** * Constructor taking a specific webdriver container name and tag * @param dockerImageName Name of the selenium docker image */ public BrowserWebDriverContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(COMPATIBLE_IMAGES); waitingFor(getDefaultWaitStrategy()); withRecordingFileFactory(new DefaultRecordingFileFactory()); // We have to force SKIP mode for the recording by default because we don't know if the image has VNC or not recordingMode = VncRecordingMode.SKIP; } @NotNull @Override protected Set getLivenessCheckPorts() { Integer seleniumPort = getMappedPort(SELENIUM_PORT); if (recordingMode == VncRecordingMode.SKIP) { return ImmutableSet.of(seleniumPort); } else { return ImmutableSet.of(seleniumPort, getMappedPort(VNC_PORT)); } } @Override protected void configure() { if (recordingMode != VncRecordingMode.SKIP) { if (vncRecordingDirectory == null) { try { vncRecordingDirectory = Files.createTempDirectory(TC_TEMP_DIR_PREFIX).toFile(); } catch (IOException e) { // should never happen as per javadoc, since we use valid prefix logger().error("Exception while trying to create temp directory", e); throw new ContainerLaunchException("Exception while trying to create temp directory", e); } } if (getNetwork() == null) { withNetwork(Network.SHARED); } vncRecordingContainer = new VncRecordingContainer(this) .withVncPassword(DEFAULT_PASSWORD) .withVncPort(VNC_PORT) .withVideoFormat(recordingFormat); } String timeZone = System.getProperty("user.timezone"); if (timeZone == null || timeZone.isEmpty()) { timeZone = "Etc/UTC"; } addExposedPorts(SELENIUM_PORT, VNC_PORT); addEnv("TZ", timeZone); if (!getEnvMap().containsKey(NO_PROXY_KEY)) { addEnv(NO_PROXY_KEY, "localhost"); } setCommand("/opt/bin/entry_point.sh"); if (getShmSize() == null) { if (SystemUtils.IS_OS_WINDOWS) { withSharedMemorySize(512 * FileUtils.ONE_MB); } else { this.getBinds().add(new Bind("/dev/shm", new Volume("/dev/shm"), AccessMode.rw)); } } /* * Some unreliability of the selenium browser containers has been observed, so allow multiple attempts to start. */ setStartupAttempts(3); } public URL getSeleniumAddress() { try { return new URL("http", getHost(), getMappedPort(SELENIUM_PORT), "/wd/hub"); } catch (MalformedURLException e) { e.printStackTrace(); // TODO return null; } } public String getVncAddress() { return "vnc://vnc:secret@" + getHost() + ":" + getMappedPort(VNC_PORT); } @Override protected void containerIsStarted(InspectContainerResponse containerInfo) { if (vncRecordingContainer != null) { LOGGER.debug("Starting VNC recording"); vncRecordingContainer.start(); } } @Override public void afterTest(TestDescription description, Optional throwable) { retainRecordingIfNeeded(description.getFilesystemFriendlyName(), !throwable.isPresent()); } @Override public void stop() { if (vncRecordingContainer != null) { try { vncRecordingContainer.stop(); } catch (Exception e) { LOGGER.debug("Failed to stop vncRecordingContainer", e); } vncRecordingContainer = null; } super.stop(); } private void retainRecordingIfNeeded(String prefix, boolean succeeded) { final boolean shouldRecord; switch (recordingMode) { case RECORD_ALL: shouldRecord = true; break; case RECORD_FAILING: shouldRecord = !succeeded; break; default: shouldRecord = false; break; } if (shouldRecord) { File recordingFile = recordingFileFactory.recordingFileForTest( vncRecordingDirectory, prefix, succeeded, vncRecordingContainer.getVideoFormat() ); LOGGER.info("Screen recordings for test {} will be stored at: {}", prefix, recordingFile); vncRecordingContainer.saveRecordingToFile(recordingFile); } } public BrowserWebDriverContainer withRecordingMode(VncRecordingMode recordingMode, File vncRecordingDirectory) { return withRecordingMode(recordingMode, vncRecordingDirectory, null); } public BrowserWebDriverContainer withRecordingMode( VncRecordingMode recordingMode, File vncRecordingDirectory, VncRecordingFormat recordingFormat ) { this.recordingMode = recordingMode; this.vncRecordingDirectory = vncRecordingDirectory; this.recordingFormat = recordingFormat; return self(); } public BrowserWebDriverContainer withRecordingFileFactory(RecordingFileFactory recordingFileFactory) { this.recordingFileFactory = recordingFileFactory; return self(); } private WaitStrategy getDefaultWaitStrategy() { final WaitStrategy logWaitStrategy = new LogMessageWaitStrategy() .withRegEx( ".*(RemoteWebDriver instances should connect to|Selenium Server is up and running|Started Selenium Standalone).*\n" ) .withStartupTimeout(Duration.ofMinutes(1)); return new WaitAllStrategy() .withStrategy(logWaitStrategy) .withStrategy(new HostPortWaitStrategy()) .withStartupTimeout(Duration.ofMinutes(1)); } public enum VncRecordingMode { SKIP, RECORD_ALL, RECORD_FAILING, } } ================================================ FILE: modules/selenium/src/test/java/org/testcontainers/containers/DefaultRecordingFileFactoryTest.java ================================================ package org.testcontainers.containers; import lombok.Value; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.params.ParameterizedClass; import org.junit.jupiter.params.provider.MethodSource; import java.io.File; import java.nio.file.Files; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @ParameterizedClass @MethodSource("data") @Value class DefaultRecordingFileFactoryTest { private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("YYYYMMdd-HHmmss"); private final DefaultRecordingFileFactory factory = new DefaultRecordingFileFactory(); private final String methodName; private final String prefix; private final boolean success; public static Collection data() { Collection args = new ArrayList<>(); args.add(new Object[] { "testMethod1", "FAILED", Boolean.FALSE }); args.add(new Object[] { "testMethod2", "PASSED", Boolean.TRUE }); return args; } @Test public void recordingFileThatShouldDescribeTheTestResultAtThePresentTime(TestInfo testInfo) throws Exception { File vncRecordingDirectory = Files.createTempDirectory("recording").toFile(); String className = testInfo.getTestClass().orElseThrow(IllegalStateException::new).getSimpleName(); String description = className + "-" + methodName; File recordingFile = factory.recordingFileForTest(vncRecordingDirectory, description, success); String expectedFilePrefix = String.format("%s-%s-%s", prefix, getClass().getSimpleName(), methodName); List expectedPossibleFileNames = Arrays.asList( new File( vncRecordingDirectory, String.format("%s-%s.flv", expectedFilePrefix, LocalDateTime.now().format(DATETIME_FORMATTER)) ), new File( vncRecordingDirectory, String.format( "%s-%s.flv", expectedFilePrefix, LocalDateTime.now().minusSeconds(1L).format(DATETIME_FORMATTER) ) ) ); assertThat(expectedPossibleFileNames).contains(recordingFile); } } ================================================ FILE: modules/selenium/src/test/java/org/testcontainers/junit/SeleniumStartTest.java ================================================ package org.testcontainers.junit; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.Parameter; import org.junit.jupiter.params.ParameterizedClass; import org.junit.jupiter.params.provider.MethodSource; import org.openqa.selenium.chrome.ChromeOptions; import org.testcontainers.containers.BrowserWebDriverContainer; import org.testcontainers.utility.DockerImageName; /** * Simple test to check that readiness detection works correctly across major versions of the containers. */ @ParameterizedClass(name = "tag: {0}") @MethodSource("data") public class SeleniumStartTest { public static String[] data() { return new String[] { "4.0.0", "3.4.0", "2.53.0" }; } @Parameter public String tag; @Test void testAdditionalStartupString() { final DockerImageName imageName = DockerImageName.parse("selenium/standalone-chrome").withTag(tag); try ( BrowserWebDriverContainer chrome = new BrowserWebDriverContainer<>(imageName) .withCapabilities(new ChromeOptions()) ) { chrome.start(); } } } ================================================ FILE: modules/selenium/src/test/java/org/testcontainers/junit/SeleniumUtilsTest.java ================================================ package org.testcontainers.junit; import org.junit.jupiter.api.Test; import org.testcontainers.containers.SeleniumUtils; import java.io.IOException; import java.util.jar.Manifest; import static org.assertj.core.api.Assertions.assertThat; /** * Created by Julien LAMY */ class SeleniumUtilsTest { @Test void detectSeleniumVersionUnder3() throws IOException { checkSeleniumVersionDetected("manifests/MANIFEST-2.45.0.MF", "2.45.0"); } @Test void detectSeleniumVersionUpper3() throws IOException { checkSeleniumVersionDetected("manifests/MANIFEST-3.5.2.MF", "3.5.2"); } /** * Check if Selenium Version detected is the correct one. * @param urlManifest : manifest file * @throws IOException */ private void checkSeleniumVersionDetected(String urlManifest, String expectedVersion) throws IOException { Manifest manifest = new Manifest(); manifest.read(this.getClass().getClassLoader().getResourceAsStream(urlManifest)); String seleniumVersion = SeleniumUtils.getSeleniumVersionFromManifest(manifest); assertThat(seleniumVersion) .as("Check if Selenium Version detected is the correct one.") .isEqualTo(expectedVersion); } } ================================================ FILE: modules/selenium/src/test/java/org/testcontainers/selenium/BaseWebDriverContainerTest.java ================================================ package org.testcontainers.selenium; import org.openqa.selenium.By; import org.openqa.selenium.Capabilities; import org.openqa.selenium.WebElement; import org.openqa.selenium.remote.RemoteWebDriver; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; import org.testcontainers.utility.DockerImageName; import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; public class BaseWebDriverContainerTest { public static Network NETWORK = Network.newNetwork(); public static GenericContainer HELLO_WORLD = new GenericContainer<>( DockerImageName.parse("testcontainers/helloworld:1.1.0") ) .withNetwork(NETWORK) .withNetworkAliases("helloworld") .withExposedPorts(8080, 8081) .waitingFor(new HttpWaitStrategy()); static { HELLO_WORLD.start(); } protected static void doSimpleExplore(BrowserWebDriverContainer rule, Capabilities capabilities) { RemoteWebDriver driver = new RemoteWebDriver(rule.getSeleniumAddress(), capabilities); driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(30)); System.out.println("Selenium remote URL is: " + rule.getSeleniumAddress()); System.out.println("VNC URL is: " + rule.getVncAddress()); driver.get("http://helloworld:8080"); WebElement title = driver.findElement(By.tagName("h1")); assertThat(title.getText().trim()) .as("the index page contains the title 'Hello world'") .isEqualTo("Hello world"); driver.quit(); } protected void assertBrowserNameIs( BrowserWebDriverContainer container, String expectedName, Capabilities capabilities ) { RemoteWebDriver driver = new RemoteWebDriver(container.getSeleniumAddress(), capabilities); driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(30)); String actual = driver.getCapabilities().getBrowserName(); assertThat(actual).as(String.format("actual browser name is %s", actual)).isEqualTo(expectedName); driver.quit(); } } ================================================ FILE: modules/selenium/src/test/java/org/testcontainers/selenium/BrowserWebDriverContainerTest.java ================================================ package org.testcontainers.selenium; import com.github.dockerjava.api.command.InspectContainerResponse; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.SystemUtils; import org.junit.jupiter.api.Test; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assumptions.assumeThat; class BrowserWebDriverContainerTest { private static final String NO_PROXY_KEY = "no_proxy"; private static final String NO_PROXY_VALUE = "localhost,.noproxy-domain.com"; @Test void honorPresetNoProxyEnvironment() { try ( BrowserWebDriverContainer chromeWithNoProxySet = new BrowserWebDriverContainer( "selenium/standalone-chrome:4.13.0" ) .withEnv(NO_PROXY_KEY, NO_PROXY_VALUE) ) { chromeWithNoProxySet.start(); Object noProxy = chromeWithNoProxySet.getEnvMap().get(NO_PROXY_KEY); assertThat(noProxy).as("no_proxy should be preserved by the container rule").isEqualTo(NO_PROXY_VALUE); } } @Test void provideDefaultNoProxyEnvironmentIfNotSet() { try ( BrowserWebDriverContainer chromeWithoutNoProxySet = new BrowserWebDriverContainer( "selenium/standalone-chrome:4.13.0" ) ) { chromeWithoutNoProxySet.start(); Object noProxy = chromeWithoutNoProxySet.getEnvMap().get(NO_PROXY_KEY); assertThat(noProxy).as("no_proxy should be set to default if not already present").isEqualTo("localhost"); } } @Test void createContainerWithShmVolume() { assumeThat(SystemUtils.IS_OS_WINDOWS).isTrue(); try ( BrowserWebDriverContainer webDriverContainer = new BrowserWebDriverContainer( "selenium/standalone-firefox:4.13.0" ) ) { webDriverContainer.start(); final List shmVolumes = shmVolumes(webDriverContainer); assertThat(shmVolumes).as("Only one shm mount present").hasSize(1); assertThat(shmVolumes.get(0).getSource()).as("Shm mount source is correct").isEqualTo("/dev/shm"); assertThat(shmVolumes.get(0).getMode()).as("Shm mount mode is correct").isEqualTo("rw"); } } @Test void createContainerWithoutShmVolume() { try ( BrowserWebDriverContainer webDriverContainer = new BrowserWebDriverContainer( "selenium/standalone-firefox:4.13.0" ) .withSharedMemorySize(512 * FileUtils.ONE_MB) ) { webDriverContainer.start(); assertThat(webDriverContainer.getShmSize()) .as("Shared memory size is configured") .isEqualTo(512 * FileUtils.ONE_MB); assertThat(shmVolumes(webDriverContainer)).as("No shm mounts present").isEqualTo(Collections.emptyList()); } } private List shmVolumes(final BrowserWebDriverContainer container) { return container .getContainerInfo() .getMounts() .stream() // destination path is always /dev/shm .filter(m -> m.getDestination().getPath().equals("/dev/shm")) .collect(Collectors.toList()); } } ================================================ FILE: modules/selenium/src/test/java/org/testcontainers/selenium/ChromeRecordingWebDriverContainerTest.java ================================================ package org.testcontainers.selenium; import com.google.common.io.PatternFilenameFilter; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.openqa.selenium.chrome.ChromeOptions; import org.testcontainers.containers.DefaultRecordingFileFactory; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.VncRecordingContainer; import org.testcontainers.containers.VncRecordingContainer.VncRecordingFormat; import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; import org.testcontainers.lifecycle.TestDescription; import org.testcontainers.selenium.BrowserWebDriverContainer.VncRecordingMode; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.Optional; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; class ChromeRecordingWebDriverContainerTest extends BaseWebDriverContainerTest { /** * Guaranty a minimum video length for FFmpeg re-encoding. * @see VncRecordingFormat#reencodeRecording(VncRecordingContainer, String) */ private static final int MINIMUM_VIDEO_DURATION_MILLISECONDS = 200; @Nested class ChromeThatRecordsAllTests { @TempDir public Path vncRecordingDirectory; @Test void recordingTestThatShouldBeRecordedAndRetainedInFlvFormatAsDefault() throws InterruptedException { File target = vncRecordingDirectory.toFile(); try ( // recordAll { // To do this, simply add extra parameters to the rule constructor, so video will default to FLV format: BrowserWebDriverContainer chrome = new BrowserWebDriverContainer("selenium/standalone-chrome:4.13.0") .withRecordingMode(VncRecordingMode.RECORD_ALL, target) // } .withRecordingFileFactory(new DefaultRecordingFileFactory()) .withNetwork(NETWORK) ) { File[] files = runSimpleExploreInContainer(chrome, "PASSED-.*\\.flv"); assertThat(files).as("Recorded file found").hasSize(1); } } private File[] runSimpleExploreInContainer(BrowserWebDriverContainer container, String fileNamePattern) throws InterruptedException { container.start(); TimeUnit.MILLISECONDS.sleep(MINIMUM_VIDEO_DURATION_MILLISECONDS); doSimpleExplore(container, new ChromeOptions()); container.afterTest( new TestDescription() { @Override public String getTestId() { return getFilesystemFriendlyName(); } @Override public String getFilesystemFriendlyName() { return "ChromeThatRecordsAllTests-recordingTestThatShouldBeRecordedAndRetained"; } }, Optional.empty() ); return vncRecordingDirectory.toFile().listFiles(new PatternFilenameFilter(fileNamePattern)); } @Test void recordingTestShouldHaveFlvExtension() throws InterruptedException { File target = vncRecordingDirectory.toFile(); try ( // recordFlv { // Set (explicitly) FLV format for recorded video: BrowserWebDriverContainer chrome = new BrowserWebDriverContainer("selenium/standalone-chrome:4.13.0") .withRecordingMode(VncRecordingMode.RECORD_ALL, target, VncRecordingFormat.FLV) // } .withRecordingFileFactory(new DefaultRecordingFileFactory()) .withNetwork(NETWORK) ) { File[] files = runSimpleExploreInContainer(chrome, "PASSED-.*\\.flv"); assertThat(files).as("Recorded file found").hasSize(1); } } @Test void recordingTestShouldHaveMp4Extension() throws InterruptedException { File target = vncRecordingDirectory.toFile(); try ( // recordMp4 { // Set MP4 format for recorded video: BrowserWebDriverContainer chrome = new BrowserWebDriverContainer("selenium/standalone-chrome:4.13.0") .withRecordingMode(VncRecordingMode.RECORD_ALL, target, VncRecordingFormat.MP4) // } .withRecordingFileFactory(new DefaultRecordingFileFactory()) .withNetwork(NETWORK) ) { File[] files = runSimpleExploreInContainer(chrome, "PASSED-.*\\.mp4"); assertThat(files).as("Recorded file found").hasSize(1); } } @Test void recordingTestThatShouldHaveCorrectDuration() throws IOException, InterruptedException { MountableFile mountableFile; try ( BrowserWebDriverContainer chrome = new BrowserWebDriverContainer("selenium/standalone-chrome:4.13.0") .withRecordingMode(VncRecordingMode.RECORD_ALL, vncRecordingDirectory.toFile()) .withRecordingFileFactory(new DefaultRecordingFileFactory()) .withNetwork(NETWORK) ) { File[] recordedFiles = runSimpleExploreInContainer(chrome, "PASSED-.*\\.flv"); mountableFile = MountableFile.forHostPath(recordedFiles[0].getCanonicalPath()); } try ( GenericContainer container = new GenericContainer<>( DockerImageName.parse("testcontainers/vnc-recorder:1.3.0") ) ) { String recordFileContainerPath = "/tmp/chromeTestRecord.flv"; container .withCopyFileToContainer(mountableFile, recordFileContainerPath) .withCreateContainerCmdModifier(createContainerCmd -> createContainerCmd.withEntrypoint("ffmpeg")) .withCommand("-i", recordFileContainerPath, "-f", "null", "-") .waitingFor( new LogMessageWaitStrategy() .withRegEx(".*Duration.*") .withStartupTimeout(Duration.of(60, ChronoUnit.SECONDS)) ) .start(); String ffmpegOutput = container.getLogs(); assertThat(ffmpegOutput) .as("Duration starts with 00:") .contains("Duration: 00:") .doesNotContain("Duration: 00:00:00.00"); } } } @Nested class ChromeThatRecordsFailingTests { @TempDir public Path vncRecordingDirectory; @Test void recordingTestThatShouldBeRecordedButNotPersisted() { try ( // withRecordingFileFactory { BrowserWebDriverContainer chrome = new BrowserWebDriverContainer("selenium/standalone-chrome:4.13.0") // } // withRecordingFileFactory { .withRecordingFileFactory(new CustomRecordingFileFactory()) // } .withNetwork(NETWORK) ) { chrome.start(); doSimpleExplore(chrome, new ChromeOptions()); } } @Test void recordingTestThatShouldBeRecordedAndRetained() throws InterruptedException { File target = vncRecordingDirectory.toFile(); try ( // recordFailing { // or if you only want videos for test failures: BrowserWebDriverContainer chrome = new BrowserWebDriverContainer("selenium/standalone-chrome:4.13.0") .withRecordingMode(VncRecordingMode.RECORD_FAILING, target) // } .withRecordingFileFactory(new DefaultRecordingFileFactory()) .withNetwork(NETWORK) ) { chrome.start(); TimeUnit.MILLISECONDS.sleep(MINIMUM_VIDEO_DURATION_MILLISECONDS); doSimpleExplore(chrome, new ChromeOptions()); chrome.afterTest( new TestDescription() { @Override public String getTestId() { return getFilesystemFriendlyName(); } @Override public String getFilesystemFriendlyName() { return "ChromeThatRecordsFailingTests-recordingTestThatShouldBeRecordedAndRetained"; } }, Optional.of(new RuntimeException("Force writing of video file.")) ); String[] files = vncRecordingDirectory.toFile().list(new PatternFilenameFilter("FAILED-.*\\.flv")); assertThat(files).as("recorded file count").hasSize(1); } } class CustomRecordingFileFactory extends DefaultRecordingFileFactory {} } } ================================================ FILE: modules/selenium/src/test/java/org/testcontainers/selenium/ChromeWebDriverContainerTest.java ================================================ package org.testcontainers.selenium; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.openqa.selenium.chrome.ChromeOptions; class ChromeWebDriverContainerTest extends BaseWebDriverContainerTest { // junitRule { public BrowserWebDriverContainer chrome = new BrowserWebDriverContainer("selenium/standalone-chrome:4.13.0") // } .withNetwork(NETWORK); @BeforeEach public void checkBrowserIsIndeedChrome() { chrome.start(); assertBrowserNameIs(chrome, "chrome", new ChromeOptions()); } @Test void simpleExploreTest() { doSimpleExplore(chrome, new ChromeOptions()); } } ================================================ FILE: modules/selenium/src/test/java/org/testcontainers/selenium/ContainerWithoutCapabilitiesTest.java ================================================ package org.testcontainers.selenium; import org.junit.jupiter.api.AutoClose; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.openqa.selenium.chrome.ChromeOptions; class ContainerWithoutCapabilitiesTest extends BaseWebDriverContainerTest { @AutoClose public BrowserWebDriverContainer chrome = new BrowserWebDriverContainer("selenium/standalone-chrome:4.13.0") .withNetwork(NETWORK); @BeforeEach public void setUp() { chrome.start(); } @Test void chromeIsStartedIfNoCapabilitiesProvided() { assertBrowserNameIs(chrome, "chrome", new ChromeOptions()); } @Test void simpleExploreTestWhenNoCapabilitiesProvided() { doSimpleExplore(chrome, new ChromeOptions()); } } ================================================ FILE: modules/selenium/src/test/java/org/testcontainers/selenium/CustomWaitTimeoutWebDriverContainerTest.java ================================================ package org.testcontainers.selenium; import org.junit.jupiter.api.Test; import org.openqa.selenium.chrome.ChromeOptions; import java.time.Duration; import java.time.temporal.ChronoUnit; class CustomWaitTimeoutWebDriverContainerTest extends BaseWebDriverContainerTest { public BrowserWebDriverContainer chromeWithCustomTimeout = new BrowserWebDriverContainer( "selenium/standalone-chrome:4.13.0" ) .withStartupTimeout(Duration.of(30, ChronoUnit.SECONDS)) .withNetwork(NETWORK); @Test void simpleExploreTest() { chromeWithCustomTimeout.start(); doSimpleExplore(chromeWithCustomTimeout, new ChromeOptions()); } } ================================================ FILE: modules/selenium/src/test/java/org/testcontainers/selenium/EdgeWebDriverContainerTest.java ================================================ package org.testcontainers.selenium; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.openqa.selenium.edge.EdgeOptions; class EdgeWebDriverContainerTest extends BaseWebDriverContainerTest { // junitRule { public BrowserWebDriverContainer edge = new BrowserWebDriverContainer("selenium/standalone-edge:4.13.0") // } .withNetwork(NETWORK); @BeforeEach public void checkBrowserIsIndeedMSEdge() { edge.start(); assertBrowserNameIs(edge, "msedge", new EdgeOptions()); } @Test void simpleExploreTest() { doSimpleExplore(edge, new EdgeOptions()); } } ================================================ FILE: modules/selenium/src/test/java/org/testcontainers/selenium/FirefoxWebDriverContainerTest.java ================================================ package org.testcontainers.selenium; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.openqa.selenium.firefox.FirefoxOptions; class FirefoxWebDriverContainerTest extends BaseWebDriverContainerTest { // junitRule { public BrowserWebDriverContainer firefox = new BrowserWebDriverContainer("selenium/standalone-firefox:4.13.0") // } .withNetwork(NETWORK); @BeforeEach public void checkBrowserIsIndeedFirefox() { firefox.start(); assertBrowserNameIs(firefox, "firefox", new FirefoxOptions()); } @Test void simpleExploreTest() { doSimpleExplore(firefox, new FirefoxOptions()); } } ================================================ FILE: modules/selenium/src/test/java/org/testcontainers/selenium/LocalServerWebDriverContainerTest.java ================================================ package org.testcontainers.selenium; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mortbay.jetty.Server; import org.mortbay.jetty.bio.SocketConnector; import org.mortbay.jetty.handler.ResourceHandler; import org.openqa.selenium.By; import org.openqa.selenium.chrome.ChromeOptions; import org.openqa.selenium.remote.RemoteWebDriver; import org.testcontainers.Testcontainers; import static org.assertj.core.api.Assertions.assertThat; /** * Test that a browser running in a container can access a web server hosted on the host machine (i.e. the one running * the tests) */ class LocalServerWebDriverContainerTest { public BrowserWebDriverContainer chrome = new BrowserWebDriverContainer("selenium/standalone-chrome:4.13.0") .withAccessToHost(true); private int localPort; @BeforeEach public void setupLocalServer() throws Exception { chrome.start(); // Set up a local Jetty HTTP server Server server = new Server(); server.addConnector(new SocketConnector()); ResourceHandler resourceHandler = new ResourceHandler(); resourceHandler.setResourceBase("src/test/resources/server"); server.addHandler(resourceHandler); server.start(); // The server will have a random port assigned, so capture that localPort = server.getConnectors()[0].getLocalPort(); } @Test void testConnection() { // getWebDriver { RemoteWebDriver driver = new RemoteWebDriver(chrome.getSeleniumAddress(), new ChromeOptions()); // } // Construct a URL that the browser container can access // getPage { Testcontainers.exposeHostPorts(localPort); driver.get("http://host.testcontainers.internal:" + localPort); // } String headingText = driver.findElement(By.cssSelector("h1")).getText().trim(); assertThat(headingText) .as("The hardcoded success message was found on a page fetched from a local server") .isEqualTo("It worked"); } } ================================================ FILE: modules/selenium/src/test/java/org/testcontainers/selenium/SpecificImageNameWebDriverContainerTest.java ================================================ package org.testcontainers.selenium; import org.junit.jupiter.api.Test; import org.openqa.selenium.firefox.FirefoxOptions; import org.testcontainers.utility.DockerImageName; class SpecificImageNameWebDriverContainerTest extends BaseWebDriverContainerTest { private static final DockerImageName FIREFOX_IMAGE = DockerImageName.parse("selenium/standalone-firefox:4.10.0"); public BrowserWebDriverContainer firefox = new BrowserWebDriverContainer(FIREFOX_IMAGE).withNetwork(NETWORK); @Test void simpleExploreTest() { firefox.start(); doSimpleExplore(firefox, new FirefoxOptions()); } } ================================================ FILE: modules/selenium/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/selenium/src/test/resources/manifests/MANIFEST-2.45.0.MF ================================================ Manifest-Version: 1.0 Built-By: linman Build-Jdk: 1.7.0_65 Created-By: Apache Maven 3.1.1 Archiver-Version: Plexus Archiver Name: Build-Info Selenium-Revision: 5017cb8e7ca8e37638dc3091b2440b90a1d8686f Selenium-Version: 2.45.0 Selenium-Build-Time: 2015-02-27 09:10:26 ================================================ FILE: modules/selenium/src/test/resources/manifests/MANIFEST-3.5.2.MF ================================================ Manifest-Version: 1.0 Name: Build-Info Build-Revision: 21ac65f960 Build-Time: 2017-08-21T20:29:34.71Z Build-User: lamaille Selenium-Version: 3.5.2 ================================================ FILE: modules/selenium/src/test/resources/server/index.html ================================================ It worked

It worked

================================================ FILE: modules/solace/build.gradle ================================================ description = "Testcontainers :: Solace" dependencies { api project(':testcontainers') shaded 'org.awaitility:awaitility:4.3.0' testImplementation 'com.solacesystems:sol-jcsmp:10.29.0' testImplementation 'org.apache.qpid:qpid-jms-client:0.61.0' testImplementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5' testImplementation 'org.apache.httpcomponents:fluent-hc:4.5.14' } ================================================ FILE: modules/solace/src/main/java/org/testcontainers/solace/Service.java ================================================ package org.testcontainers.solace; /** * Services that are supported by Testcontainers implementation */ public enum Service { /** * Advanced Message Queuing Protocol */ AMQP("amqp", 5672, "amqp", false), /** * Message Queuing Telemetry Transport */ MQTT("mqtt", 1883, "tcp", false), /** * Representational State Transfer */ REST("rest", 9000, "http", false), /** * Solace Message Format */ SMF("smf", 55555, "tcp", true), /** * Solace Message Format with SSL */ SMF_SSL("smf", 55443, "tcps", true); private final String name; private final Integer port; private final String protocol; private final boolean supportSSL; Service(String name, Integer port, String protocol, boolean supportSSL) { this.name = name; this.port = port; this.protocol = protocol; this.supportSSL = supportSSL; } /** * @return Port assigned for the service */ public Integer getPort() { return this.port; } /** * @return Protocol of the service */ public String getProtocol() { return this.protocol; } /** * @return Name of the service */ public String getName() { return this.name; } /** * @return Is SSL for this service supported ? */ public boolean isSupportSSL() { return this.supportSSL; } } ================================================ FILE: modules/solace/src/main/java/org/testcontainers/solace/SolaceContainer.java ================================================ package org.testcontainers.solace; import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.api.model.Ulimit; import org.apache.commons.lang3.tuple.Pair; import org.awaitility.Awaitility; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import java.io.IOException; import java.time.Duration; import java.util.ArrayList; import java.util.List; /** * Testcontainers implementation of Solace PubSub+. *

* Supported image: {@code solace/solace-pubsub-standard} *

* Exposed ports: *

    *
  • Console: 8080
  • *
  • AMQP: 5672
  • *
  • MQTT: 1883
  • *
  • HTTP: 9000
  • *
  • SMF: 55555
  • *
  • SMF SSL: 55443
  • *
*/ public class SolaceContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("solace/solace-pubsub-standard"); private static final String DEFAULT_VPN = "default"; private static final String DEFAULT_USERNAME = "default"; private static final String SOLACE_READY_MESSAGE = ".*Running pre-startup checks:.*"; private static final String SOLACE_ACTIVE_MESSAGE = "Primary Virtual Router is now active"; private static final String TMP_SCRIPT_LOCATION = "/tmp/script.cli"; private static final Long SHM_SIZE = (long) Math.pow(1024, 3); private String username = "root"; private String password = "password"; private String vpn = DEFAULT_VPN; private final List> topicsConfiguration = new ArrayList<>(); private boolean withClientCert; /** * Create a new solace container with the specified image name. * * @param dockerImageName the image name that should be used. */ public SolaceContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } /** * Create a new solace container with the specified docker image. * * @param dockerImageName the image name that should be used. */ public SolaceContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); withCreateContainerCmdModifier(cmd -> { cmd .getHostConfig() .withShmSize(SHM_SIZE) .withUlimits(new Ulimit[] { new Ulimit("nofile", 2448L, 1048576L) }); }); waitingFor(Wait.forLogMessage(SOLACE_READY_MESSAGE, 1).withStartupTimeout(Duration.ofSeconds(60))); withExposedPorts(8080); withEnv("username_admin_globalaccesslevel", "admin"); withEnv("username_admin_password", "admin"); } @Override protected void configure() { withCopyToContainer(createConfigurationScript(), TMP_SCRIPT_LOCATION); } @Override protected void containerIsStarted(InspectContainerResponse containerInfo) { if (withClientCert) { executeCommand("cp", "/tmp/solace.pem", "/usr/sw/jail/certs/solace.pem"); executeCommand("cp", "/tmp/rootCA.crt", "/usr/sw/jail/certs/rootCA.crt"); } executeCommand("cp", TMP_SCRIPT_LOCATION, "/usr/sw/jail/cliscripts/script.cli"); waitOnCommandResult(SOLACE_ACTIVE_MESSAGE, "grep", "-R", SOLACE_ACTIVE_MESSAGE, "/usr/sw/jail/logs/system.log"); executeCommand("/usr/sw/loads/currentload/bin/cli", "-A", "-es", "script.cli"); } private Transferable createConfigurationScript() { StringBuilder scriptBuilder = new StringBuilder(); updateConfigScript(scriptBuilder, "enable"); updateConfigScript(scriptBuilder, "configure"); // Create VPN if not default if (!vpn.equals(DEFAULT_VPN)) { updateConfigScript(scriptBuilder, "create message-vpn " + vpn); updateConfigScript(scriptBuilder, "no shutdown"); updateConfigScript(scriptBuilder, "exit"); updateConfigScript(scriptBuilder, "client-profile default message-vpn " + vpn); updateConfigScript(scriptBuilder, "message-spool"); updateConfigScript(scriptBuilder, "allow-guaranteed-message-send"); updateConfigScript(scriptBuilder, "allow-guaranteed-message-receive"); updateConfigScript(scriptBuilder, "allow-guaranteed-endpoint-create"); updateConfigScript(scriptBuilder, "allow-guaranteed-endpoint-create-durability all"); updateConfigScript(scriptBuilder, "exit"); updateConfigScript(scriptBuilder, "exit"); updateConfigScript(scriptBuilder, "message-spool message-vpn " + vpn); updateConfigScript(scriptBuilder, "max-spool-usage 60000"); updateConfigScript(scriptBuilder, "exit"); } // Configure username and password if (username.equals(DEFAULT_USERNAME)) { throw new RuntimeException("Cannot override password for default client"); } updateConfigScript(scriptBuilder, "create client-username " + username + " message-vpn " + vpn); updateConfigScript(scriptBuilder, "password " + password); updateConfigScript(scriptBuilder, "no shutdown"); updateConfigScript(scriptBuilder, "exit"); if (withClientCert) { // Client certificate authority configuration updateConfigScript(scriptBuilder, "authentication"); updateConfigScript(scriptBuilder, "create client-certificate-authority RootCA"); updateConfigScript(scriptBuilder, "certificate file rootCA.crt"); updateConfigScript(scriptBuilder, "show client-certificate-authority ca-name *"); updateConfigScript(scriptBuilder, "end"); // Server certificates configuration updateConfigScript(scriptBuilder, "configure"); updateConfigScript(scriptBuilder, "ssl"); updateConfigScript(scriptBuilder, "server-certificate solace.pem"); updateConfigScript(scriptBuilder, "cipher-suite msg-backbone name AES128-SHA"); updateConfigScript(scriptBuilder, "exit"); updateConfigScript(scriptBuilder, "message-vpn " + vpn); // Enable client certificate authentication updateConfigScript(scriptBuilder, "authentication client-certificate"); updateConfigScript(scriptBuilder, "allow-api-provided-username"); updateConfigScript(scriptBuilder, "no shutdown"); updateConfigScript(scriptBuilder, "end"); } else { // Configure VPN Basic authentication updateConfigScript(scriptBuilder, "message-vpn " + vpn); updateConfigScript(scriptBuilder, "authentication basic auth-type internal"); updateConfigScript(scriptBuilder, "no shutdown"); updateConfigScript(scriptBuilder, "end"); } if (!topicsConfiguration.isEmpty()) { // Enable services updateConfigScript(scriptBuilder, "configure"); // Configure default ACL updateConfigScript(scriptBuilder, "acl-profile default message-vpn " + vpn); // Configure default action to disallow updateConfigScript(scriptBuilder, "subscribe-topic default-action disallow"); updateConfigScript(scriptBuilder, "publish-topic default-action disallow"); updateConfigScript(scriptBuilder, "exit"); updateConfigScript(scriptBuilder, "message-vpn " + vpn); updateConfigScript(scriptBuilder, "service"); for (Pair topicConfig : topicsConfiguration) { Service service = topicConfig.getValue(); String topicName = topicConfig.getKey(); updateConfigScript(scriptBuilder, service.getName()); if (service.isSupportSSL()) { if (withClientCert) { updateConfigScript(scriptBuilder, "ssl"); } else { updateConfigScript(scriptBuilder, "plain-text"); } } updateConfigScript(scriptBuilder, "no shutdown"); updateConfigScript(scriptBuilder, "end"); // Add publish/subscribe topic exceptions updateConfigScript(scriptBuilder, "configure"); updateConfigScript(scriptBuilder, "acl-profile default message-vpn " + vpn); updateConfigScript( scriptBuilder, String.format("publish-topic exceptions %s list %s", service.getName(), topicName) ); updateConfigScript( scriptBuilder, String.format("subscribe-topic exceptions %s list %s", service.getName(), topicName) ); updateConfigScript(scriptBuilder, "end"); } } return Transferable.of(scriptBuilder.toString()); } private void executeCommand(String... command) { try { ExecResult execResult = execInContainer(command); if (execResult.getExitCode() != 0) { logCommandError(execResult.getStderr(), command); } } catch (IOException | InterruptedException e) { logCommandError(e.getMessage(), command); } } private void updateConfigScript(StringBuilder scriptBuilder, String command) { scriptBuilder.append(command).append("\n"); } private void waitOnCommandResult(String waitingFor, String... command) { Awaitility .await() .pollInterval(Duration.ofMillis(500)) .timeout(Duration.ofSeconds(30)) .until(() -> { try { return execInContainer(command).getStdout().contains(waitingFor); } catch (IOException | InterruptedException e) { logCommandError(e.getMessage(), command); return true; } }); } private void logCommandError(String error, String... command) { logger().error("Could not execute command {}: {}", command, error); } /** * Sets the client credentials * * @param username Client username * @param password Client password * @return This container. */ public SolaceContainer withCredentials(final String username, final String password) { this.username = username; this.password = password; return this; } /** * Adds the topic configuration * * @param topic Name of the topic * @param service Service to be supported on provided topic * @return This container. */ public SolaceContainer withTopic(String topic, Service service) { topicsConfiguration.add(Pair.of(topic, service)); addExposedPort(service.getPort()); return this; } /** * Sets the VPN name * * @param vpn VPN name * @return This container. */ public SolaceContainer withVpn(String vpn) { this.vpn = vpn; return this; } /** * Sets the solace server ceritificates * * @param certFile Server certificate * @param caFile Certified Authority certificate * @return This container. */ public SolaceContainer withClientCert(final MountableFile certFile, final MountableFile caFile) { this.withClientCert = true; return withCopyFileToContainer(certFile, "/tmp/solace.pem").withCopyFileToContainer(caFile, "/tmp/rootCA.crt"); } /** * Configured VPN * * @return the configured VPN that should be used for connections */ public String getVpn() { return this.vpn; } /** * Host address for provided service * * @param service - service for which host needs to be retrieved * @return host address exposed from the container */ public String getOrigin(Service service) { return String.format("%s://%s:%s", service.getProtocol(), getHost(), getMappedPort(service.getPort())); } /** * Configured username * * @return the standard username that should be used for connections */ public String getUsername() { return this.username; } /** * Configured password * * @return the standard password that should be used for connections */ public String getPassword() { return this.password; } } ================================================ FILE: modules/solace/src/test/java/org/testcontainers/solace/SolaceContainerAMQPTest.java ================================================ package org.testcontainers.solace; import org.apache.qpid.jms.JmsConnectionFactory; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import javax.jms.Connection; import javax.jms.ConnectionFactory; import javax.jms.JMSException; import javax.jms.MessageConsumer; import javax.jms.MessageProducer; import javax.jms.Session; import javax.jms.TextMessage; import javax.jms.Topic; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; class SolaceContainerAMQPTest { private static final Logger LOGGER = LoggerFactory.getLogger(SolaceContainerAMQPTest.class); private static final String MESSAGE = "HelloWorld"; private static final String TOPIC_NAME = "Topic/ActualTopic"; @Test void testSolaceContainer() throws JMSException { try ( SolaceContainer solaceContainer = new SolaceContainer("solace/solace-pubsub-standard:10.25.0") .withTopic(TOPIC_NAME, Service.AMQP) .withVpn("amqp-vpn") ) { solaceContainer.start(); // solaceContainerUsage { Session session = createSession( solaceContainer.getUsername(), solaceContainer.getPassword(), solaceContainer.getOrigin(Service.AMQP) ); // } assertThat(session).isNotNull(); assertThat(consumeMessageFromSolace(session)).isEqualTo(MESSAGE); session.close(); } } private static Session createSession(String username, String password, String host) { try { ConnectionFactory connectionFactory = new JmsConnectionFactory(username, password, host); Connection connection = connectionFactory.createConnection(); Session session = connection.createSession(); connection.start(); return session; } catch (Exception e) { fail("Error connecting and setting up session! " + e.getMessage()); return null; } } private void publishMessageToSolace(Session session, Topic topic) throws JMSException { MessageProducer messageProducer = session.createProducer(topic); TextMessage message = session.createTextMessage(MESSAGE); messageProducer.send(message); messageProducer.close(); } private String consumeMessageFromSolace(Session session) { CountDownLatch latch = new CountDownLatch(1); try { String[] result = new String[1]; Topic topic = session.createTopic(TOPIC_NAME); MessageConsumer messageConsumer = session.createConsumer(topic); messageConsumer.setMessageListener(message -> { try { if (message instanceof TextMessage) { result[0] = ((TextMessage) message).getText(); } latch.countDown(); } catch (Exception e) { LOGGER.error("Exception received: " + e.getMessage()); latch.countDown(); } }); publishMessageToSolace(session, topic); assertThat(latch.await(10L, TimeUnit.SECONDS)).isTrue(); messageConsumer.close(); return result[0]; } catch (Exception e) { throw new RuntimeException("Cannot receive message from solace", e); } } } ================================================ FILE: modules/solace/src/test/java/org/testcontainers/solace/SolaceContainerMQTTTest.java ================================================ package org.testcontainers.solace; import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; import org.eclipse.paho.client.mqttv3.MqttCallback; import org.eclipse.paho.client.mqttv3.MqttClient; import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; class SolaceContainerMQTTTest { private static final Logger LOGGER = LoggerFactory.getLogger(SolaceContainerMQTTTest.class); private static final String MESSAGE = "HelloWorld"; private static final String TOPIC_NAME = "Topic/ActualTopic"; @Test void testSolaceContainer() { try ( SolaceContainer solaceContainer = new SolaceContainer("solace/solace-pubsub-standard:10.25.0") .withTopic(TOPIC_NAME, Service.MQTT) .withVpn("mqtt-vpn") ) { solaceContainer.start(); MqttClient client = createClient( solaceContainer.getUsername(), solaceContainer.getPassword(), solaceContainer.getOrigin(Service.MQTT) ); assertThat(client).isNotNull(); assertThat(consumeMessageFromSolace(client)).isEqualTo(MESSAGE); } } private static MqttClient createClient(String username, String password, String host) { try { MqttClient mqttClient = new MqttClient(host, MESSAGE); MqttConnectOptions connOpts = new MqttConnectOptions(); connOpts.setCleanSession(true); connOpts.setUserName(username); connOpts.setPassword(password.toCharArray()); mqttClient.connect(connOpts); return mqttClient; } catch (Exception e) { fail("Error connecting and setting up session! " + e.getMessage()); return null; } } private void publishMessageToSolace(MqttClient mqttClient) throws MqttException { MqttMessage message = new MqttMessage(MESSAGE.getBytes()); message.setQos(0); mqttClient.publish(TOPIC_NAME, message); } private String consumeMessageFromSolace(MqttClient client) { CountDownLatch latch = new CountDownLatch(1); try { String[] result = new String[1]; client.setCallback( new MqttCallback() { @Override public void connectionLost(Throwable cause) { LOGGER.error("Exception received: " + cause.getMessage()); latch.countDown(); } @Override public void messageArrived(String topic, MqttMessage message) { result[0] = new String(message.getPayload()); latch.countDown(); } @Override public void deliveryComplete(IMqttDeliveryToken token) {} } ); client.subscribe(TOPIC_NAME, 0); publishMessageToSolace(client); assertThat(latch.await(10L, TimeUnit.SECONDS)).isTrue(); return result[0]; } catch (Exception e) { throw new RuntimeException("Cannot receive message from solace", e); } } } ================================================ FILE: modules/solace/src/test/java/org/testcontainers/solace/SolaceContainerRESTTest.java ================================================ package org.testcontainers.solace; import org.apache.http.HttpHeaders; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.CredentialsProvider; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; import org.junit.jupiter.api.Test; import java.io.IOException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; class SolaceContainerRESTTest { private static final String MESSAGE = "HelloWorld"; private static final String TOPIC_NAME = "Topic/ActualTopic"; @Test void testSolaceContainer() throws IOException { try ( SolaceContainer solaceContainer = new SolaceContainer("solace/solace-pubsub-standard:10.25.0") .withTopic(TOPIC_NAME, Service.REST) .withVpn("rest-vpn") ) { solaceContainer.start(); testPublishMessageToSolace(solaceContainer, Service.REST); } } private void testPublishMessageToSolace(SolaceContainer solaceContainer, Service service) throws IOException { HttpClient client = createClient(solaceContainer); HttpPost request = new HttpPost(solaceContainer.getOrigin(service) + "/" + TOPIC_NAME); request.setEntity(new StringEntity(MESSAGE)); request.addHeader(HttpHeaders.CONTENT_TYPE, "text/plain"); HttpResponse response = client.execute(request); if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { fail("Cannot send message to solace - " + EntityUtils.toString(response.getEntity())); } assertThat(EntityUtils.toString(response.getEntity())).isEmpty(); } private HttpClient createClient(SolaceContainer solaceContainer) { CredentialsProvider provider = new BasicCredentialsProvider(); provider.setCredentials( AuthScope.ANY, new UsernamePasswordCredentials(solaceContainer.getUsername(), solaceContainer.getPassword()) ); return HttpClientBuilder.create().setDefaultCredentialsProvider(provider).build(); } } ================================================ FILE: modules/solace/src/test/java/org/testcontainers/solace/SolaceContainerSMFTest.java ================================================ package org.testcontainers.solace; import com.solacesystems.jcsmp.BytesXMLMessage; import com.solacesystems.jcsmp.ConsumerFlowProperties; import com.solacesystems.jcsmp.EndpointProperties; import com.solacesystems.jcsmp.JCSMPException; import com.solacesystems.jcsmp.JCSMPFactory; import com.solacesystems.jcsmp.JCSMPProperties; import com.solacesystems.jcsmp.JCSMPSession; import com.solacesystems.jcsmp.JCSMPStreamingPublishCorrelatingEventHandler; import com.solacesystems.jcsmp.Queue; import com.solacesystems.jcsmp.TextMessage; import com.solacesystems.jcsmp.Topic; import com.solacesystems.jcsmp.XMLMessageConsumer; import com.solacesystems.jcsmp.XMLMessageListener; import com.solacesystems.jcsmp.XMLMessageProducer; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.utility.MountableFile; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; class SolaceContainerSMFTest { private static final Logger LOGGER = LoggerFactory.getLogger(SolaceContainerSMFTest.class); private static final String MESSAGE = "HelloWorld"; private static final Topic TOPIC = JCSMPFactory.onlyInstance().createTopic("Topic/ActualTopic"); private static final Queue QUEUE = JCSMPFactory.onlyInstance().createQueue("Queue"); @Test void testSolaceContainerWithSimpleAuthentication() { try ( // solaceContainerSetup { SolaceContainer solaceContainer = new SolaceContainer("solace/solace-pubsub-standard:10.25.0") .withCredentials("user", "pass") .withTopic(TOPIC.getName(), Service.SMF) .withVpn("test_vpn") // } ) { solaceContainer.start(); JCSMPSession session = createSessionWithBasicAuth(solaceContainer); assertThat(session).isNotNull(); consumeMessageFromTopics(session); session.closeSession(); } } @Test void testSolaceContainerWithCreateFlow() { try ( SolaceContainer solaceContainer = new SolaceContainer("solace/solace-pubsub-standard:10.25.0") .withCredentials("user", "pass") .withTopic(TOPIC.getName(), Service.SMF) .withVpn("test_vpn") ) { solaceContainer.start(); JCSMPSession session = createSessionWithBasicAuth(solaceContainer); assertThat(session).isNotNull(); testCreateFlow(session); session.closeSession(); } } private static void testCreateFlow(JCSMPSession session) { try { EndpointProperties endpointProperties = new EndpointProperties(); endpointProperties.setAccessType(EndpointProperties.ACCESSTYPE_NONEXCLUSIVE); endpointProperties.setQuota(1000); session.provision(QUEUE, endpointProperties, JCSMPSession.FLAG_IGNORE_ALREADY_EXISTS); session.addSubscription(QUEUE, TOPIC, JCSMPSession.WAIT_FOR_CONFIRM); ConsumerFlowProperties flowProperties = new ConsumerFlowProperties().setEndpoint(QUEUE); TestConsumer listener = new TestConsumer(); session.createFlow(listener, flowProperties).start(); publishMessageToSolaceTopic(session); listener.waitForMessage(); } catch (Exception e) { throw new RuntimeException("Cannot process message using solace topic/queue: " + e.getMessage(), e); } } @Test void testSolaceContainerWithCertificates() { try ( // solaceContainerUsageSSL { SolaceContainer solaceContainer = new SolaceContainer("solace/solace-pubsub-standard:10.25.0") .withClientCert( MountableFile.forClasspathResource("solace.pem"), MountableFile.forClasspathResource("rootCA.crt") ) .withTopic(TOPIC.getName(), Service.SMF_SSL) // } ) { solaceContainer.start(); JCSMPSession session = createSessionWithCertificates(solaceContainer); assertThat(session).isNotNull(); consumeMessageFromTopics(session); session.closeSession(); } } private String getResourceFileLocation(String name) { return getClass().getClassLoader().getResource(name).getPath(); } private static JCSMPSession createSessionWithBasicAuth(SolaceContainer solace) { JCSMPProperties properties = new JCSMPProperties(); properties.setProperty(JCSMPProperties.HOST, solace.getOrigin(Service.SMF)); properties.setProperty(JCSMPProperties.VPN_NAME, solace.getVpn()); properties.setProperty(JCSMPProperties.USERNAME, solace.getUsername()); properties.setProperty(JCSMPProperties.PASSWORD, solace.getPassword()); return createSession(properties); } private JCSMPSession createSessionWithCertificates(SolaceContainer solace) { JCSMPProperties properties = new JCSMPProperties(); properties.setProperty(JCSMPProperties.HOST, solace.getOrigin(Service.SMF_SSL)); properties.setProperty(JCSMPProperties.VPN_NAME, solace.getVpn()); properties.setProperty(JCSMPProperties.USERNAME, solace.getUsername()); // Just for testing purposes properties.setProperty(JCSMPProperties.SSL_VALIDATE_CERTIFICATE_HOST, false); properties.setProperty(JCSMPProperties.SSL_VALIDATE_CERTIFICATE, true); properties.setProperty(JCSMPProperties.SSL_VALIDATE_CERTIFICATE_DATE, true); properties.setProperty( JCSMPProperties.AUTHENTICATION_SCHEME, JCSMPProperties.AUTHENTICATION_SCHEME_CLIENT_CERTIFICATE ); properties.setProperty(JCSMPProperties.SSL_TRUST_STORE, getResourceFileLocation("truststore")); properties.setProperty(JCSMPProperties.SSL_TRUST_STORE_PASSWORD, "solace"); properties.setProperty(JCSMPProperties.SSL_KEY_STORE, getResourceFileLocation("client.pfx")); properties.setProperty(JCSMPProperties.SSL_KEY_STORE_PASSWORD, "solace"); return createSession(properties); } private static JCSMPSession createSession(JCSMPProperties properties) { try { JCSMPSession session = JCSMPFactory.onlyInstance().createSession(properties); session.connect(); return session; } catch (Exception e) { fail("Error connecting and setting up session! " + e.getMessage()); return null; } } private static void publishMessageToSolaceTopic(JCSMPSession session) throws JCSMPException { XMLMessageProducer producer = session.getMessageProducer( new JCSMPStreamingPublishCorrelatingEventHandler() { @Override public void responseReceivedEx(Object o) { LOGGER.info("Producer received response for msg: " + o); } @Override public void handleErrorEx(Object o, JCSMPException e, long l) { LOGGER.error(String.format("Producer received error for msg: %s - %s", o, e)); } } ); TextMessage msg = producer.createTextMessage(); msg.setText(MESSAGE); producer.send(msg, TOPIC); } private static void consumeMessageFromTopics(JCSMPSession session) { try { TestConsumer listener = new TestConsumer(); XMLMessageConsumer cons = session.getMessageConsumer(listener); session.addSubscription(TOPIC); cons.start(); publishMessageToSolaceTopic(session); listener.waitForMessage(); } catch (Exception e) { throw new RuntimeException("Cannot process message using solace: " + e.getMessage(), e); } } static class TestConsumer implements XMLMessageListener { private final CountDownLatch latch = new CountDownLatch(1); private String result; @Override public void onReceive(BytesXMLMessage msg) { if (msg instanceof TextMessage) { TextMessage textMessage = (TextMessage) msg; String message = textMessage.getText(); result = message; LOGGER.info("Message received: " + message); } latch.countDown(); } @Override public void onException(JCSMPException e) { LOGGER.error("Exception received: " + e.getMessage()); latch.countDown(); } private void waitForMessage() { try { assertThat(latch.await(10L, TimeUnit.SECONDS)).isTrue(); assertThat(result).isEqualTo(MESSAGE); } catch (Exception e) { throw new RuntimeException("Cannot receive message from solace: " + e.getMessage(), e); } } } } ================================================ FILE: modules/solace/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/solace/src/test/resources/rootCA.crt ================================================ -----BEGIN CERTIFICATE----- MIIDmjCCAoKgAwIBAgIIVP5+fnVAfbwwDQYJKoZIhvcNAQELBQAwRjELMAkGA1UE BhMCUEwxFzAVBgNVBAoTDnRlc3Rjb250YWluZXJzMR4wHAYDVQQDExV0ZXN0Y29u dGFpbmVycy1zb2xhY2UwIBcNMjQwMTI0MTYyNDAwWhgPMjIwMDAxMDEwMDAwMDBa MEYxCzAJBgNVBAYTAlBMMRcwFQYDVQQKEw50ZXN0Y29udGFpbmVyczEeMBwGA1UE AxMVdGVzdGNvbnRhaW5lcnMtc29sYWNlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A MIIBCgKCAQEAr26PCDd4C7BsSh2aMn/c7ZLLX44FnQE2JqakGGt9JGzkWDQaTAJk 8HlBnRvJAq03RsOq0hazhbW6NGOt//SuMqS0WxT/p8wyIaIMZy8H/nCJm4Uw88/i AG9rE/mQJpwNucrB3FBVihDBZRystYyO781dgOK7+lnW6YKXJ3jRK/QJOkuJs3F1 jtgcZJHNwCoy7hRzWzPKEjb67aGDRVs6Iq6TL94AIEw4Qdb+x9g7Vtdgzj6M9stJ AKJe9TJRTAbVoKRw6ROOSbpYFePS3DI71bS87Ho2erXBRSovnfioh4u7bZH4XPwu MIZvfeSVoKiIk2anReD+tM+zI/iU5bmCwQIDAQABo4GJMIGGMA8GA1UdEwEB/wQF MAMBAf8wHQYDVR0OBBYEFNvFeVvpx8cEIIcjvDtQIOIPr9W8MAsGA1UdDwQEAwIC jDAUBgNVHREEDTALgglsb2NhbGhvc3QwEQYJYIZIAYb4QgEBBAQDAgAHMB4GCWCG SAGG+EIBDQQRFg94Y2EgY2VydGlmaWNhdGUwDQYJKoZIhvcNAQELBQADggEBAGiI 7qyjIx6ssKi04pKi0cuUZWI+JeY/+nkLEXN9dgbSchFcVJMpV8l9VVqINNFt9LMX aTzQLnjUYop6osHA16zF9WViVJ8LUzwec70A4J5gA3tiebLNWCoAp7v5+dRPUc8r 0oi4QTP4Siu6kVjZFNWuuezUZyaVz+rZuEe+rVWyNakIiAzIacaK3FBAtcCYcF2L 2CjHCCsrRgmmvkRydGsr0pJEub9wxY4kVzQS07CwB0OMxllZm65ZtVdpPWh4f3GG /bB6kL9InDfHfSleKG60HpEPGqwWFm6VQxQVIOoumyP1WDp3ix+u0u5Y/Mo6hakm LplIUWZWT4rv/NaB/Rw= -----END CERTIFICATE----- ================================================ FILE: modules/solace/src/test/resources/solace.pem ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEA43Us+KsRZ218C8JAzB7LiF5dhx95F/spQzkiFgSNJ97215Go SIPzDWm1ac4wTGZR1YHiTk2PEFHk21TeJi5sODGHqR0G0Qr2IweSz52Zs5h0p+Xw O3x55In/Dx+jIMdOR9HfuU05WlYSTWXdwFyq9VmmXK+ViJ0pZE9X18E1Qtm27S6v FIckdEKswh17DCKMQ6kRMDkLwe7yOoBYPAFw0R46o3PXAuVHbkI/3ewn/aopOQ+4 I0dqTxp7m7spdIanT+oSd5EN4G1a/oTYPO67qbcq/SIgWLHR/iT2dOE28ftAco64 +N5ds5wz5a0JL7CwIdxcgSsndsBSR+hM5cC5kwIDAQABAoIBAERwMkrT9hWfrK5B EYwZS/ZJJm0MvDvJ931hiG8FiY9QmAb+rZq8EPqdLteaEZA7TS4nuXcEASLQ8UJJ Q9pLJ4a06HOq5y0o1ixuD+9mJSQToC4Qknrjli18lADx7Pxk25nifSVdJf+XXERr fRBvEYVnJxZGQoDrgNPMx8qEOMlW9Wub9+O5OttsMdxpVeWw/G36mT8JOVZvIw9W frRXNuX9F17lJZIt3E10QrmKSdyogPdTMXBTTw5TCIMck1KH+lRSwP7GRnJMHKNE bcq+yoePYHdWfIW87I8bFprgC3IyCumA+ERp3oWNvTEziXkt1bogWx4pe+tFAhIp flpCDFkCgYEA8jhvgqEYuX9I7tY5gJHeC5BOg7d0XNpP4RN9cIEYkamgcwil7DAf hAn2+Lzvf1wcBtFi4wyygCsXsdP3CeV60vFYHWrgXozlZVs5NIDTb4CWNbylSapR mlfhfCQQAGXP8WdKQdoXsGhSgbX3kuAaW7d9BscPgPUSqCbSMvuRKt8CgYEA8GW+ agfK7EDRbYbLRNcof4UIOnDoZ+vi62tbQ0wRI9dt0dXDgyeUuPMoeD5uTSu1jcjf T59UIKHwZO5f6A7ADZeJLU72Oo4dCYxG3H3ZM1VwW6kqVlWOtBkwOoibb5ApUjEP jVCtvRNAn+htDO+4nhQDK2xwgJnkOaXCWFZgO80CgYBjPkRSHXdn6YMUeKmuyBVW X5YL2crPkJNSAQ5QXlSWug2HlG+HSmBfVUXfvGnUoQTKtlfx923bncxjjBmX8HJW o5Qa2YN8ufXzhWD25iG7edARzG1ctXAh8QfuOUhlIVIF8vA18wnpuZS0mL4La87g 7VlIwZ7Uk5VFWEKfqPtduQKBgQDgqjWKYj4DDZCsC41siKgQhQNrmpmYhZtM6Mgh 3LUoCe1Yba6KpDMZpiXsOmxbMr46A8CvaPf2h2Fi8mQvO5nBGh3ZejIkByyb/705 02Np1i9rem1Wwh7bsa6hBYo+eTwk1DT0nLHCQnvi9hT0QhUHpyxPKMj7ZtckCQXY COFnAQKBgQCnPzxlGmmv9ktOgDN6pOzTd3VLtthQu+9u7AeumUjC4al69ROfqNW2 dfzJcdlJqMDfuHDFle+fO0Ahfp3j/BYSZFf7jAzo/jMVh32siYiq6C6MawzknwsS nUBfPFBD84Fkv5YaD0TT48p7YUANFL18Roq1PjezPdmkn7T8Nuaz2g== -----END RSA PRIVATE KEY----- -----BEGIN CERTIFICATE----- MIIDqTCCApGgAwIBAgIIIFE70OcBFc8wDQYJKoZIhvcNAQELBQAwRjELMAkGA1UE BhMCUEwxFzAVBgNVBAoTDnRlc3Rjb250YWluZXJzMR4wHAYDVQQDExV0ZXN0Y29u dGFpbmVycy1zb2xhY2UwIBcNMjQwMTI0MTYyNDAwWhgPMjIwMDAxMDEwMDAwMDBa MDkxFzAVBgNVBAoTDnRlc3Rjb250YWluZXJzMR4wHAYDVQQDExV0ZXN0Y29udGFp bmVycy1zb2xhY2UwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDjdSz4 qxFnbXwLwkDMHsuIXl2HH3kX+ylDOSIWBI0n3vbXkahIg/MNabVpzjBMZlHVgeJO TY8QUeTbVN4mLmw4MYepHQbRCvYjB5LPnZmzmHSn5fA7fHnkif8PH6Mgx05H0d+5 TTlaVhJNZd3AXKr1WaZcr5WInSlkT1fXwTVC2bbtLq8UhyR0QqzCHXsMIoxDqREw OQvB7vI6gFg8AXDRHjqjc9cC5UduQj/d7Cf9qik5D7gjR2pPGnubuyl0hqdP6hJ3 kQ3gbVr+hNg87ruptyr9IiBYsdH+JPZ04Tbx+0Byjrj43l2znDPlrQkvsLAh3FyB Kyd2wFJH6EzlwLmTAgMBAAGjgaUwgaIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU T5AeIO1T10daj92c9R0qALbPu+cwCwYDVR0PBAQDAgPoMB0GA1UdJQQWMBQGCCsG AQUFBwMBBggrBgEFBQcDAjAUBgNVHREEDTALgglsb2NhbGhvc3QwEQYJYIZIAYb4 QgEBBAQDAgZAMB4GCWCGSAGG+EIBDQQRFg94Y2EgY2VydGlmaWNhdGUwDQYJKoZI hvcNAQELBQADggEBADyiXuXEx/Cka/xDxUmluYprTMubhf2Tt840WtXODoqa6mK0 L74M18fvOeRL6G1ufzyrOUg5kt+0vyS8LlNmPJ/F7RbFvJ7Ca+vFVChi6NLag5U7 Xp6bdI7jwLKiP1Sh+AyucZafW9k+e3PO4beAJqYHmYxsLtXmnYlWFL/6VoYACpTu 6qFYenAecqQAs49Ujs5y9l0IRC8Hj9ZcPVf63BTceAdiPqwUhIctD4vB6v1pTwjb CaQxJLY5P3sQNkvMMSK29p03EJOMZ4oVt09RBBEm1YwwUA5pp/JaxJvRlMgnTNHs h6xQyX9XEWS9cNUa2VktBEq1eoIg/njpnbiGvDo= -----END CERTIFICATE----- ================================================ FILE: modules/solr/build.gradle ================================================ description = "Testcontainers :: Solr" dependencies { api project(':testcontainers') // TODO use JDK's HTTP client and/or Apache HttpClient5 shaded 'com.squareup.okhttp3:okhttp:5.3.2' testImplementation 'org.apache.solr:solr-solrj:8.11.4' } ================================================ FILE: modules/solr/src/main/java/org/testcontainers/containers/SolrClientUtils.java ================================================ package org.testcontainers.containers; import okhttp3.HttpUrl; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import org.apache.commons.io.IOUtils; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; /** * Utils class which can create collections and configurations. */ public class SolrClientUtils { private static OkHttpClient httpClient = new OkHttpClient(); /** * Creates a new configuration and uploads the solrconfig.xml and schema.xml * * @param hostname the Hostname under which solr is reachable * @param port the Port on which solr is running * @param configurationName the name of the configuration which should be created * @param solrConfig the url under which the solrconfig.xml can be found * @param solrSchema the url under which the schema.xml can be found or null if the default schema should be used */ public static void uploadConfiguration( String hostname, int port, String configurationName, URL solrConfig, URL solrSchema ) throws URISyntaxException, IOException { Map parameters = new HashMap<>(); parameters.put("action", "UPLOAD"); parameters.put("name", configurationName); HttpUrl url = generateSolrURL(hostname, port, Arrays.asList("admin", "configs"), parameters); byte[] configurationZipFile = generateConfigZipFile(solrConfig, solrSchema); executePost(url, configurationZipFile); } /** * Creates a new collection * * @param hostname the Hostname under which solr is reachable * @param port The Port on which solr is running * @param collectionName the name of the collection which should be created * @param configurationName the name of the configuration which should used to create the collection * or null if the default configuration should be used */ public static void createCollection(String hostname, int port, String collectionName, String configurationName) throws URISyntaxException, IOException { Map parameters = new HashMap<>(); parameters.put("action", "CREATE"); parameters.put("name", collectionName); parameters.put("numShards", "1"); parameters.put("replicationFactor", "1"); parameters.put("wt", "json"); if (configurationName != null) { parameters.put("collection.configName", configurationName); } HttpUrl url = generateSolrURL(hostname, port, Arrays.asList("admin", "collections"), parameters); executePost(url, null); } private static void executePost(HttpUrl url, byte[] data) throws IOException { RequestBody requestBody = data == null ? RequestBody.create(MediaType.parse("text/plain"), "") : RequestBody.create(MediaType.parse("application/octet-stream"), data); Request request = new Request.Builder().url(url).post(requestBody).build(); Response response = httpClient.newCall(request).execute(); if (!response.isSuccessful()) { String responseBody = ""; if (response.body() != null) { responseBody = response.body().string(); response.close(); } throw new SolrClientUtilsException(response.code(), "Unable to upload binary\n" + responseBody); } if (response.body() != null) { response.close(); } } private static HttpUrl generateSolrURL( String hostname, int port, List pathSegments, Map parameters ) throws URISyntaxException { HttpUrl.Builder builder = new HttpUrl.Builder(); builder.scheme("http"); builder.host(hostname); builder.port(port); // Path builder.addPathSegment("solr"); if (pathSegments != null) { pathSegments.forEach(builder::addPathSegment); } // Query Parameters parameters.forEach(builder::addQueryParameter); return builder.build(); } private static byte[] generateConfigZipFile(URL solrConfiguration, URL solrSchema) throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ZipOutputStream zipOutputStream = new ZipOutputStream(bos); // SolrConfig zipOutputStream.putNextEntry(new ZipEntry("solrconfig.xml")); IOUtils.copy(solrConfiguration.openStream(), zipOutputStream); zipOutputStream.closeEntry(); // Solr Schema if (solrSchema != null) { zipOutputStream.putNextEntry(new ZipEntry("schema.xml")); IOUtils.copy(solrSchema.openStream(), zipOutputStream); zipOutputStream.closeEntry(); } zipOutputStream.close(); return bos.toByteArray(); } } ================================================ FILE: modules/solr/src/main/java/org/testcontainers/containers/SolrClientUtilsException.java ================================================ package org.testcontainers.containers; public class SolrClientUtilsException extends RuntimeException { public SolrClientUtilsException(int statusCode, String msg) { super("Http Call Status: " + statusCode + "\n" + msg); } } ================================================ FILE: modules/solr/src/main/java/org/testcontainers/containers/SolrContainer.java ================================================ package org.testcontainers.containers; import com.github.dockerjava.api.command.InspectContainerResponse; import lombok.SneakyThrows; import org.apache.commons.lang3.StringUtils; import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; import org.testcontainers.utility.ComparableVersion; import org.testcontainers.utility.DockerImageName; import java.net.URL; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.HashSet; import java.util.Set; /** * Testcontainers implementation for Solr. *

* Supported image: {@code solr} *

* Exposed ports: *

    *
  • Solr: 8983
  • *
  • Zookeeper: 9983
  • *
* * @deprecated use {@link org.testcontainers.solr.SolrContainer} instead. */ public class SolrContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("solr"); @Deprecated public static final String DEFAULT_TAG = "8.3.0"; public static final Integer ZOOKEEPER_PORT = 9983; public static final Integer SOLR_PORT = 8983; private SolrContainerConfiguration configuration; private final ComparableVersion imageVersion; /** * @deprecated use {@link #SolrContainer(DockerImageName)} instead */ @Deprecated public SolrContainer() { this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } public SolrContainer(final String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public SolrContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); this.waitStrategy = new LogMessageWaitStrategy() .withRegEx(".*o\\.e\\.j\\.s\\.Server Started.*") .withStartupTimeout(Duration.of(60, ChronoUnit.SECONDS)); this.configuration = new SolrContainerConfiguration(); this.imageVersion = new ComparableVersion(dockerImageName.getVersionPart()); } public SolrContainer withZookeeper(boolean zookeeper) { configuration.setZookeeper(zookeeper); return self(); } public SolrContainer withCollection(String collection) { if (StringUtils.isEmpty(collection)) { throw new IllegalArgumentException("Collection name must not be empty"); } configuration.setCollectionName(collection); return self(); } public SolrContainer withConfiguration(String name, URL solrConfig) { if (StringUtils.isEmpty(name) || solrConfig == null) { throw new IllegalArgumentException(); } configuration.setConfigurationName(name); configuration.setSolrConfiguration(solrConfig); return self(); } public SolrContainer withSchema(URL schema) { configuration.setSolrSchema(schema); return self(); } public int getSolrPort() { return getMappedPort(SOLR_PORT); } public int getZookeeperPort() { return getMappedPort(ZOOKEEPER_PORT); } @Override @SneakyThrows protected void configure() { if (configuration.getSolrSchema() != null && configuration.getSolrConfiguration() == null) { throw new IllegalStateException("Solr needs to have a configuration if you want to use a schema"); } // Generate Command Builder String command = "solr start -f"; // Add Default Ports this.addExposedPort(SOLR_PORT); // Configure Zookeeper if (configuration.isZookeeper()) { this.addExposedPort(ZOOKEEPER_PORT); if (this.imageVersion.isGreaterThanOrEqualTo("9.7.0")) { command = "-DzkRun --host localhost"; } else { command = "-DzkRun -h localhost"; } } // Apply generated Command this.setCommand(command); } @Override public Set getLivenessCheckPortNumbers() { return new HashSet<>(getSolrPort()); } @Override protected void waitUntilContainerStarted() { getWaitStrategy().waitUntilReady(this); } @Override @SneakyThrows protected void containerIsStarted(InspectContainerResponse containerInfo) { if (!configuration.isZookeeper()) { ExecResult result = execInContainer("solr", "create", "-c", configuration.getCollectionName()); if (result.getExitCode() != 0) { throw new IllegalStateException( "Unable to create solr core:\nStdout: " + result.getStdout() + "\nStderr:" + result.getStderr() ); } return; } if (StringUtils.isNotEmpty(configuration.getConfigurationName())) { SolrClientUtils.uploadConfiguration( getHost(), getSolrPort(), configuration.getConfigurationName(), configuration.getSolrConfiguration(), configuration.getSolrSchema() ); } SolrClientUtils.createCollection( getHost(), getSolrPort(), configuration.getCollectionName(), configuration.getConfigurationName() ); } } ================================================ FILE: modules/solr/src/main/java/org/testcontainers/containers/SolrContainerConfiguration.java ================================================ package org.testcontainers.containers; import lombok.Data; import java.net.URL; @Data public class SolrContainerConfiguration { private boolean zookeeper = true; private String collectionName = "dummy"; private String configurationName; private URL solrConfiguration; private URL solrSchema; } ================================================ FILE: modules/solr/src/main/java/org/testcontainers/solr/SolrContainer.java ================================================ package org.testcontainers.solr; import com.github.dockerjava.api.command.InspectContainerResponse; import lombok.SneakyThrows; import org.apache.commons.lang3.StringUtils; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.SolrClientUtils; import org.testcontainers.containers.SolrContainerConfiguration; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.ComparableVersion; import org.testcontainers.utility.DockerImageName; import java.net.URL; import java.time.Duration; import java.util.HashSet; import java.util.Set; /** * Testcontainers implementation for Solr. *

* Supported image: {@code solr} *

* Exposed ports: *

    *
  • Solr: 8983
  • *
  • Zookeeper: 9983
  • *
*/ public class SolrContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("solr"); public static final Integer ZOOKEEPER_PORT = 9983; public static final Integer SOLR_PORT = 8983; private SolrContainerConfiguration configuration; private final ComparableVersion imageVersion; public SolrContainer(final String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public SolrContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); waitingFor( Wait.forLogMessage(".*o\\.e\\.j\\.s\\.Server Started.*", 1).withStartupTimeout(Duration.ofMinutes(1)) ); this.configuration = new SolrContainerConfiguration(); this.imageVersion = new ComparableVersion(dockerImageName.getVersionPart()); } public SolrContainer withZookeeper(boolean zookeeper) { configuration.setZookeeper(zookeeper); return self(); } public SolrContainer withCollection(String collection) { if (StringUtils.isEmpty(collection)) { throw new IllegalArgumentException("Collection name must not be empty"); } configuration.setCollectionName(collection); return self(); } public SolrContainer withConfiguration(String name, URL solrConfig) { if (StringUtils.isEmpty(name) || solrConfig == null) { throw new IllegalArgumentException(); } configuration.setConfigurationName(name); configuration.setSolrConfiguration(solrConfig); return self(); } public SolrContainer withSchema(URL schema) { configuration.setSolrSchema(schema); return self(); } public int getSolrPort() { return getMappedPort(SOLR_PORT); } public int getZookeeperPort() { return getMappedPort(ZOOKEEPER_PORT); } @Override @SneakyThrows protected void configure() { if (configuration.getSolrSchema() != null && configuration.getSolrConfiguration() == null) { throw new IllegalStateException("Solr needs to have a configuration if you want to use a schema"); } // Generate Command Builder String command = "solr start -f"; // Add Default Ports addExposedPort(SOLR_PORT); // Configure Zookeeper if (configuration.isZookeeper()) { addExposedPort(ZOOKEEPER_PORT); if (this.imageVersion.isGreaterThanOrEqualTo("9.7.0")) { command = "-DzkRun --host localhost"; } else { command = "-DzkRun -h localhost"; } } // Apply generated Command setCommand(command); } @Override public Set getLivenessCheckPortNumbers() { return new HashSet<>(getSolrPort()); } @Override protected void waitUntilContainerStarted() { getWaitStrategy().waitUntilReady(this); } @Override @SneakyThrows protected void containerIsStarted(InspectContainerResponse containerInfo) { if (!configuration.isZookeeper()) { ExecResult result = execInContainer("solr", "create", "-c", configuration.getCollectionName()); if (result.getExitCode() != 0) { throw new IllegalStateException( "Unable to create solr core:\nStdout: " + result.getStdout() + "\nStderr:" + result.getStderr() ); } return; } if (StringUtils.isNotEmpty(configuration.getConfigurationName())) { SolrClientUtils.uploadConfiguration( getHost(), getSolrPort(), configuration.getConfigurationName(), configuration.getSolrConfiguration(), configuration.getSolrSchema() ); } SolrClientUtils.createCollection( getHost(), getSolrPort(), configuration.getCollectionName(), configuration.getConfigurationName() ); } } ================================================ FILE: modules/solr/src/test/java/org/testcontainers/solr/SolrContainerTest.java ================================================ package org.testcontainers.solr; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.impl.Http2SolrClient; import org.apache.solr.client.solrj.response.SolrPingResponse; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import java.io.IOException; import static org.assertj.core.api.Assertions.assertThat; class SolrContainerTest { private SolrClient client = null; public static String[] getVersionsToTest() { return new String[] { "solr:8.11.4", "solr:9.8.0" }; } @AfterEach void stopRestClient() throws IOException { if (client != null) { client.close(); client = null; } } @ParameterizedTest @MethodSource("getVersionsToTest") void solrCloudTest(String solrImage) throws IOException, SolrServerException { try (SolrContainer container = new SolrContainer(solrImage)) { container.start(); SolrPingResponse response = getClient(container).ping("dummy"); assertThat(response.getStatus()).isZero(); assertThat(response.jsonStr()).contains("zkConnected\":true"); } } @ParameterizedTest @MethodSource("getVersionsToTest") void solrStandaloneTest(String solrImage) throws IOException, SolrServerException { try (SolrContainer container = new SolrContainer(solrImage).withZookeeper(false)) { container.start(); SolrPingResponse response = getClient(container).ping("dummy"); assertThat(response.getStatus()).isZero(); assertThat(response.jsonStr()).contains("zkConnected\":null"); } } @ParameterizedTest @MethodSource("getVersionsToTest") void solrCloudPingTest(String solrImage) throws IOException, SolrServerException { // solrContainerUsage { // Create the solr container. SolrContainer container = new SolrContainer(solrImage); // Start the container. This step might take some time... container.start(); // Do whatever you want with the client ... SolrClient client = new Http2SolrClient.Builder( "http://" + container.getHost() + ":" + container.getSolrPort() + "/solr" ) .build(); SolrPingResponse response = client.ping("dummy"); // Stop the container. container.stop(); // } } private SolrClient getClient(SolrContainer container) { if (client == null) { client = new Http2SolrClient.Builder("http://" + container.getHost() + ":" + container.getSolrPort() + "/solr") .build(); } return client; } } ================================================ FILE: modules/solr/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/spock/build.gradle ================================================ plugins { id 'groovy' } description = "Testcontainers :: Spock-Extension" dependencies { api project(':testcontainers') implementation 'org.spockframework:spock-core:2.3-groovy-4.0' testImplementation project(':testcontainers-selenium') testImplementation project(':testcontainers-mysql') testImplementation project(':testcontainers-postgresql') testImplementation 'com.zaxxer:HikariCP:7.0.2' testImplementation 'org.apache.httpcomponents:httpclient:4.5.14' testRuntimeOnly 'org.postgresql:postgresql:42.7.8' testRuntimeOnly 'com.mysql:mysql-connector-j:9.5.0' testRuntimeOnly platform('org.junit:junit-bom:5.14.1') testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testRuntimeOnly 'org.junit.platform:junit-platform-testkit' testCompileOnly 'org.jetbrains:annotations:26.0.2-1' } tasks.withType(GroovyCompile) { sourceCompatibility = '1.8' targetCompatibility = '1.8' options.encoding = 'UTF-8' } sourceJar { /* allJava is default (see gradle/publishing.gradle:sourceJar) allSource contains both .java and .groovy files */ from sourceSets.main.allSource } javadocJar { dependsOn groovydoc archiveClassifier = 'javadoc' from groovydoc.destinationDir } ================================================ FILE: modules/spock/src/main/groovy/org/testcontainers/spock/DockerAvailableDetector.groovy ================================================ package org.testcontainers.spock import org.testcontainers.DockerClientFactory class DockerAvailableDetector { boolean isDockerAvailable() { try { DockerClientFactory.instance().client(); return true; } catch (Throwable ex) { return false; } } } ================================================ FILE: modules/spock/src/main/groovy/org/testcontainers/spock/SpockTestDescription.groovy ================================================ package org.testcontainers.spock import groovy.transform.PackageScope import org.spockframework.runtime.extension.IMethodInvocation import org.testcontainers.lifecycle.TestDescription /** * Spock specific implementation of a Testcontainers TestDescription. * * Filesystem friendly name is based on Specification and Feature. */ @PackageScope class SpockTestDescription implements TestDescription { String specName String featureName static SpockTestDescription fromTestDescription(IMethodInvocation invocation) { return new SpockTestDescription([ specName: invocation.spec.name, featureName: invocation.feature.name ]) } @Override String getTestId() { return getFilesystemFriendlyName() } @Override String getFilesystemFriendlyName() { return [specName, featureName].collect { URLEncoder.encode(it, 'UTF-8') }.join('-') } } ================================================ FILE: modules/spock/src/main/groovy/org/testcontainers/spock/Testcontainers.groovy ================================================ package org.testcontainers.spock import org.spockframework.runtime.extension.ExtensionAnnotation import java.lang.annotation.ElementType import java.lang.annotation.Inherited import java.lang.annotation.Retention import java.lang.annotation.RetentionPolicy import java.lang.annotation.Target /** * {@code @Testcontainers} is a Spock extension to activate automatic * startup and stop of containers used in a test case. * *

The Testcontainers extension finds all fields that extend * {@link org.testcontainers.containers.GenericContainer} or * {@link org.testcontainers.containers.DockerComposeContainer} and calls their * container lifecycle methods. Containers annotated with {@link spock.lang.Shared} * will be shared between test methods. They will be * started only once before any test method is executed and stopped after the * last test method has executed. Containers without {@link spock.lang.Shared} * annotation will be started and stopped for every test method.

* *

The annotation {@code @Testcontainers} can be used on a superclass in * the test hierarchy as well. All subclasses will automatically inherit * support for the extension.

* *

Example:

* *
 * @Testcontainers
 * class MyTestcontainersTests extends Specification {
 *
 *     // will be started only once in setupSpec() and stopped after last test method
 *     @Shared
 *     MySQLContainer MY_SQL_CONTAINER = new MySQLContainer()
 *
 *     // will be started before and stopped after each test method
 *     PostgreSQLContainer postgresqlContainer = new PostgreSQLContainer()
 *             .withDatabaseName('foo')
 *             .withUsername('foo')
 *             .withPassword('secret')
 *
 *     def 'test'() {
 *         expect:
 *         MY_SQL_CONTAINER.running
 *         postgresqlContainer.running
 *     }
 * }
 * 
*/ @Inherited @Retention(RetentionPolicy.RUNTIME) @Target([ElementType.TYPE, ElementType.METHOD]) @ExtensionAnnotation(TestcontainersExtension) @interface Testcontainers { /** * Whether tests should be disabled (rather than failing) when Docker is not available. Defaults to * {@code false}. * @return if the tests should be disabled when Docker is not available */ boolean disabledWithoutDocker() default false; } ================================================ FILE: modules/spock/src/main/groovy/org/testcontainers/spock/TestcontainersExtension.groovy ================================================ package org.testcontainers.spock import org.spockframework.runtime.AbstractRunListener import org.spockframework.runtime.extension.AbstractAnnotationDrivenExtension import org.spockframework.runtime.model.ErrorInfo import org.spockframework.runtime.model.SpecInfo class TestcontainersExtension extends AbstractAnnotationDrivenExtension { private final DockerAvailableDetector dockerDetector TestcontainersExtension() { this(new DockerAvailableDetector()) } TestcontainersExtension(DockerAvailableDetector dockerDetector) { this.dockerDetector = dockerDetector } @Override void visitSpecAnnotation(Testcontainers annotation, SpecInfo spec) { if (annotation.disabledWithoutDocker()) { if (!dockerDetector.isDockerAvailable()) { spec.skip("disabledWithoutDocker is true and Docker is not available") } } def listener = new ErrorListener() def interceptor = new TestcontainersMethodInterceptor(spec, listener) spec.addSetupSpecInterceptor(interceptor) spec.addCleanupSpecInterceptor(interceptor) spec.addSetupInterceptor(interceptor) spec.addCleanupInterceptor(interceptor) spec.addListener(listener) } private class ErrorListener extends AbstractRunListener { List errors = [] @Override void error(ErrorInfo error) { errors.add(error) } } } ================================================ FILE: modules/spock/src/main/groovy/org/testcontainers/spock/TestcontainersMethodInterceptor.groovy ================================================ package org.testcontainers.spock import org.spockframework.runtime.extension.AbstractMethodInterceptor import org.spockframework.runtime.extension.IMethodInvocation import org.spockframework.runtime.model.FieldInfo import org.spockframework.runtime.model.SpecInfo import org.testcontainers.containers.ComposeContainer import org.testcontainers.containers.DockerComposeContainer import org.testcontainers.containers.GenericContainer import org.testcontainers.lifecycle.TestLifecycleAware import org.testcontainers.spock.TestcontainersExtension.ErrorListener class TestcontainersMethodInterceptor extends AbstractMethodInterceptor { private final SpecInfo spec private final ErrorListener errorListener TestcontainersMethodInterceptor(SpecInfo spec, ErrorListener errorListener) { this.spec = spec this.errorListener = errorListener } @Override void interceptSetupSpecMethod(IMethodInvocation invocation) throws Throwable { def containers = findAllContainers(true) startContainers(containers, invocation) def dockerCompose = findAllDockerComposeContainers(true) startDockerComposeContainers(dockerCompose, invocation) def compose = findAllComposeContainers(true) startComposeContainers(compose, invocation) invocation.proceed() } @Override void interceptCleanupSpecMethod(IMethodInvocation invocation) throws Throwable { def containers = findAllContainers(true) stopContainers(containers, invocation) def dockerCompose = findAllDockerComposeContainers(true) stopDockerComposeContainers(dockerCompose, invocation) def compose = findAllComposeContainers(true) stopComposeContainers(compose, invocation) invocation.proceed() } @Override void interceptSetupMethod(IMethodInvocation invocation) throws Throwable { def containers = findAllContainers(false) startContainers(containers, invocation) def dockerCompose = findAllDockerComposeContainers(false) startDockerComposeContainers(dockerCompose, invocation) def compose = findAllComposeContainers(false) startComposeContainers(compose, invocation) invocation.proceed() } @Override void interceptCleanupMethod(IMethodInvocation invocation) throws Throwable { def containers = findAllContainers(false) stopContainers(containers, invocation) def dockerCompose = findAllDockerComposeContainers(false) stopDockerComposeContainers(dockerCompose, invocation) def compose = findAllComposeContainers(false) stopComposeContainers(compose, invocation) invocation.proceed() } private List findAllContainers(boolean shared) { spec.allFields.findAll { FieldInfo f -> GenericContainer.isAssignableFrom(f.type) && f.shared == shared } } private List findAllDockerComposeContainers(boolean shared) { spec.allFields.findAll { FieldInfo f -> DockerComposeContainer.isAssignableFrom(f.type) && f.shared == shared } } private List findAllComposeContainers(boolean shared) { spec.allFields.findAll { FieldInfo f -> ComposeContainer.isAssignableFrom(f.type) && f.shared == shared } } private static void startContainers(List containers, IMethodInvocation invocation) { containers.each { FieldInfo f -> GenericContainer container = readContainerFromField(f, invocation) if(!container.isRunning()){ container.start() } if (container instanceof TestLifecycleAware) { def testDescription = SpockTestDescription.fromTestDescription(invocation) (container as TestLifecycleAware).beforeTest(testDescription) } } } private void stopContainers(List containers, IMethodInvocation invocation) { containers.each { FieldInfo f -> GenericContainer container = readContainerFromField(f, invocation) if (container instanceof TestLifecycleAware) { // we assume first error is the one we want def maybeException = Optional.ofNullable(errorListener.errors[0]?.exception) def testDescription = SpockTestDescription.fromTestDescription(invocation) (container as TestLifecycleAware).afterTest(testDescription, maybeException) } container.stop() } } private static void startDockerComposeContainers(List compose, IMethodInvocation invocation) { compose.each { FieldInfo f -> DockerComposeContainer c = f.readValue(invocation.instance) as DockerComposeContainer c.start() } } private static void startComposeContainers(List compose, IMethodInvocation invocation) { compose.each { FieldInfo f -> ComposeContainer c = f.readValue(invocation.instance) as ComposeContainer c.start() } } private static void stopDockerComposeContainers(List compose, IMethodInvocation invocation) { compose.each { FieldInfo f -> DockerComposeContainer c = f.readValue(invocation.instance) as DockerComposeContainer c.stop() } } private static void stopComposeContainers(List compose, IMethodInvocation invocation) { compose.each { FieldInfo f -> ComposeContainer c = f.readValue(invocation.instance) as ComposeContainer c.stop() } } private static GenericContainer readContainerFromField(FieldInfo f, IMethodInvocation invocation) { f.readValue(invocation.instance) as GenericContainer } } ================================================ FILE: modules/spock/src/test/groovy/org/testcontainers/spock/ComposeContainerIT.groovy ================================================ package org.testcontainers.spock import org.apache.http.client.methods.HttpGet import org.apache.http.impl.client.HttpClientBuilder import org.testcontainers.containers.ComposeContainer import org.testcontainers.containers.wait.strategy.Wait import org.testcontainers.utility.DockerImageName import spock.lang.Specification @Testcontainers class ComposeContainerIT extends Specification { ComposeContainer composeContainer = new ComposeContainer( DockerImageName.parse("docker:25.0.5"), new File("src/test/resources/docker-compose.yml")) .withExposedService("whoami-1", 80, Wait.forHttp("/")) String host int port def setup() { host = composeContainer.getServiceHost("whoami-1", 80) port = composeContainer.getServicePort("whoami-1", 80) } def "running compose defined container is accessible on configured port"() { given: "a http client" def client = HttpClientBuilder.create().build() when: "accessing web server" def response = client.execute(new HttpGet("http://$host:$port")) then: "docker container is running and returns http status code 200" response.statusLine.statusCode == 200 } } ================================================ FILE: modules/spock/src/test/groovy/org/testcontainers/spock/DockerComposeContainerIT.groovy ================================================ package org.testcontainers.spock import org.apache.http.client.methods.HttpGet import org.apache.http.impl.client.HttpClientBuilder import org.testcontainers.containers.DockerComposeContainer import org.testcontainers.containers.wait.strategy.Wait import org.testcontainers.utility.DockerImageName import spock.lang.Specification @Testcontainers class DockerComposeContainerIT extends Specification { DockerComposeContainer composeContainer = new DockerComposeContainer( DockerImageName.parse("docker/compose:debian-1.29.2"), new File("src/test/resources/docker-compose.yml")) .withExposedService("whoami_1", 80, Wait.forHttp("/")) String host int port def setup() { host = composeContainer.getServiceHost("whoami_1", 80) port = composeContainer.getServicePort("whoami_1", 80) } def "running compose defined container is accessible on configured port"() { given: "a http client" def client = HttpClientBuilder.create().build() when: "accessing web server" def response = client.execute(new HttpGet("http://$host:$port")) then: "docker container is running and returns http status code 200" response.statusLine.statusCode == 200 } } ================================================ FILE: modules/spock/src/test/groovy/org/testcontainers/spock/MySqlContainerIT.groovy ================================================ package org.testcontainers.spock import org.testcontainers.containers.MySQLContainer import spock.lang.Shared import spock.lang.Specification /** * This test verifies, that setup and cleanup of containers works correctly. * It's easily achieved using the MySQLContainer, since it will fail * if the same image is running. * * @see Second container is started when stopping old container */ @Testcontainers class MySqlContainerIT extends Specification { @Shared MySQLContainer mySQLContainer = new MySQLContainer(SpockTestImages.MYSQL_IMAGE) def "dummy test"() { expect: mySQLContainer.isRunning() } } ================================================ FILE: modules/spock/src/test/groovy/org/testcontainers/spock/PostgresContainerIT.groovy ================================================ package org.testcontainers.spock import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import org.testcontainers.containers.PostgreSQLContainer import spock.lang.Shared import spock.lang.Specification import java.sql.ResultSet import java.sql.Statement // PostgresContainerIT { @Testcontainers class PostgresContainerIT extends Specification { @Shared PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer(SpockTestImages.POSTGRES_TEST_IMAGE) .withDatabaseName("foo") .withUsername("foo") .withPassword("secret") def "waits until postgres accepts jdbc connections"() { given: "a jdbc connection" HikariConfig hikariConfig = new HikariConfig() hikariConfig.setJdbcUrl(postgreSQLContainer.jdbcUrl) hikariConfig.setUsername("foo") hikariConfig.setPassword("secret") HikariDataSource ds = new HikariDataSource(hikariConfig) when: "querying the database" Statement statement = ds.getConnection().createStatement() statement.execute("SELECT 1") ResultSet resultSet = statement.getResultSet() resultSet.next() then: "result is returned" int resultSetInt = resultSet.getInt(1) resultSetInt == 1 cleanup: ds.close() } } // } ================================================ FILE: modules/spock/src/test/groovy/org/testcontainers/spock/SharedComposeContainerIT.groovy ================================================ package org.testcontainers.spock import org.apache.http.client.methods.HttpGet import org.apache.http.impl.client.HttpClientBuilder import org.testcontainers.containers.ComposeContainer import org.testcontainers.containers.wait.strategy.Wait import org.testcontainers.utility.DockerImageName import spock.lang.Shared import spock.lang.Specification @Testcontainers class SharedComposeContainerIT extends Specification { @Shared ComposeContainer composeContainer = new ComposeContainer( DockerImageName.parse("docker:25.0.5"), new File("src/test/resources/docker-compose.yml")) .withExposedService("whoami-1", 80, Wait.forHttp("/")) String host int port def setup() { host = composeContainer.getServiceHost("whoami-1", 80) port = composeContainer.getServicePort("whoami-1", 80) } def "running compose defined container is accessible on configured port"() { given: "a http client" def client = HttpClientBuilder.create().build() when: "accessing web server" def response = client.execute(new HttpGet("http://$host:$port")) then: "docker container is running and returns http status code 200" response.statusLine.statusCode == 200 } } ================================================ FILE: modules/spock/src/test/groovy/org/testcontainers/spock/SharedDockerComposeContainerIT.groovy ================================================ package org.testcontainers.spock import org.apache.http.client.methods.HttpGet import org.apache.http.impl.client.HttpClientBuilder import org.testcontainers.containers.DockerComposeContainer import org.testcontainers.containers.wait.strategy.Wait import org.testcontainers.utility.DockerImageName import spock.lang.Shared import spock.lang.Specification @Testcontainers class SharedDockerComposeContainerIT extends Specification { @Shared DockerComposeContainer composeContainer = new DockerComposeContainer( DockerImageName.parse("docker/compose:debian-1.29.2"), new File("src/test/resources/docker-compose.yml")) .withExposedService("whoami_1", 80, Wait.forHttp("/")) String host int port def setup() { host = composeContainer.getServiceHost("whoami_1", 80) port = composeContainer.getServicePort("whoami_1", 80) } def "running compose defined container is accessible on configured port"() { given: "a http client" def client = HttpClientBuilder.create().build() when: "accessing web server" def response = client.execute(new HttpGet("http://$host:$port")) then: "docker container is running and returns http status code 200" response.statusLine.statusCode == 200 } } ================================================ FILE: modules/spock/src/test/groovy/org/testcontainers/spock/SpockTestImages.groovy ================================================ package org.testcontainers.spock import org.testcontainers.utility.DockerImageName interface SpockTestImages { DockerImageName MYSQL_IMAGE = DockerImageName.parse("mysql:8.0.36") DockerImageName POSTGRES_TEST_IMAGE = DockerImageName.parse("postgres:9.6.12") DockerImageName HTTPD_IMAGE = DockerImageName.parse("httpd:2.4-alpine") DockerImageName TINY_IMAGE = DockerImageName.parse("alpine:3.17") } ================================================ FILE: modules/spock/src/test/groovy/org/testcontainers/spock/TestHierarchyIT.groovy ================================================ package org.testcontainers.spock import org.testcontainers.containers.PostgreSQLContainer import spock.lang.Shared /** * This test verifies that integration tests can subclass each other. * Also verifies that the @Testcontainers annotation is inherited. */ class TestHierarchyIT extends MySqlContainerIT { @Shared PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer(SpockTestImages.POSTGRES_TEST_IMAGE) .withDatabaseName("foo") .withUsername("foo") .withPassword("secret") def "both containers are running"() { expect: postgreSQLContainer.isRunning() mySQLContainer.isRunning() } } ================================================ FILE: modules/spock/src/test/groovy/org/testcontainers/spock/TestLifecycleAwareContainerMock.java ================================================ package org.testcontainers.spock; import org.testcontainers.containers.GenericContainer; import org.testcontainers.lifecycle.TestDescription; import org.testcontainers.lifecycle.TestLifecycleAware; import java.util.ArrayList; import java.util.List; import java.util.Optional; public class TestLifecycleAwareContainerMock extends GenericContainer implements TestLifecycleAware { static final String BEFORE_TEST = "beforeTest"; static final String AFTER_TEST = "afterTest"; final List lifecycleMethodCalls = new ArrayList<>(); final List lifecycleFilesystemFriendlyNames = new ArrayList<>(); Throwable capturedThrowable; public TestLifecycleAwareContainerMock() { super(SpockTestImages.TINY_IMAGE); } @Override public void beforeTest(TestDescription description) { lifecycleMethodCalls.add(BEFORE_TEST); lifecycleFilesystemFriendlyNames.add(description.getFilesystemFriendlyName()); } @Override public void afterTest(TestDescription description, Optional throwable) { lifecycleMethodCalls.add(AFTER_TEST); throwable.ifPresent(capturedThrowable -> this.capturedThrowable = capturedThrowable); } @Override public void start() { // Do nothing } @Override public void stop() { // Do nothing } } ================================================ FILE: modules/spock/src/test/groovy/org/testcontainers/spock/TestLifecycleAwareIT.groovy ================================================ package org.testcontainers.spock import org.intellij.lang.annotations.Language import spock.lang.Specification import spock.lang.Unroll import spock.util.EmbeddedSpecRunner class TestLifecycleAwareIT extends Specification { @Unroll("When failing test is #fails, afterTest receives '#filesystemFriendlyNames' and throwable starting with message '#errorMessageStartsWith'") def "lifecycle awareness"() { given: @Language("groovy") String myTest = """ import org.testcontainers.spock.Testcontainers import org.testcontainers.containers.GenericContainer import spock.lang.Specification @Testcontainers class TestLifecycleAwareIT extends Specification { GenericContainer container = System.properties["org.testcontainers.container"] as GenericContainer def "perform test"() { expect: !System.properties["org.testcontainers.shouldFail"] } } """ and: def container = new TestLifecycleAwareContainerMock() System.properties["org.testcontainers.container"] = container System.properties["org.testcontainers.shouldFail"] = fails when: "executing the test" def runner = new EmbeddedSpecRunner(throwFailure: false) runner.run(myTest) then: "mock container received lifecycle calls as expected" container.lifecycleMethodCalls == ["beforeTest", "afterTest"] container.lifecycleFilesystemFriendlyNames.join(",") == filesystemFriendlyNames if (errorMessageStartsWith) { assert container.capturedThrowable.message.startsWith(errorMessageStartsWith) } else { assert container.capturedThrowable == null } where: fails | filesystemFriendlyNames | errorMessageStartsWith false | 'TestLifecycleAwareIT-perform+test' | null true | 'TestLifecycleAwareIT-perform+test' | "Condition not satisfied:" } } ================================================ FILE: modules/spock/src/test/groovy/org/testcontainers/spock/TestcontainersExtensionTest.groovy ================================================ package org.testcontainers.spock import org.spockframework.runtime.model.SpecInfo import spock.lang.Specification import spock.lang.Unroll class TestcontainersExtensionTest extends Specification { @Unroll def "should handle disabledWithoutDocker=#disabledWithoutDocker and dockerAvailable=#dockerAvailable correctly"() { given: def dockerDetector = Mock(DockerAvailableDetector) dockerDetector.isDockerAvailable() >> dockerAvailable def extension = new TestcontainersExtension(dockerDetector) def specInfo = Mock(SpecInfo) def annotation = disabledWithoutDocker ? TestDisabledWithoutDocker.getAnnotation(Testcontainers) : TestEnabledWithoutDocker.getAnnotation(Testcontainers) when: extension.visitSpecAnnotation(annotation, specInfo) then: skipCalls * specInfo.skip("disabledWithoutDocker is true and Docker is not available") where: disabledWithoutDocker | dockerAvailable | skipCalls true | true | 0 true | false | 1 false | true | 0 false | false | 0 } @Testcontainers(disabledWithoutDocker = true) static class TestDisabledWithoutDocker {} @Testcontainers static class TestEnabledWithoutDocker {} } ================================================ FILE: modules/spock/src/test/groovy/org/testcontainers/spock/TestcontainersRestartBetweenTestsIT.groovy ================================================ package org.testcontainers.spock import org.testcontainers.containers.GenericContainer import spock.lang.Shared import spock.lang.Specification import spock.lang.Stepwise @Stepwise @Testcontainers class TestcontainersRestartBetweenTestsIT extends Specification { GenericContainer genericContainer = new GenericContainer(SpockTestImages.HTTPD_IMAGE) .withExposedPorts(80) @Shared String lastContainerId def "retrieving first id"() { when: lastContainerId = genericContainer.containerId then: true } def "containers is recreated between tests"() { expect: genericContainer.containerId != lastContainerId } } ================================================ FILE: modules/spock/src/test/groovy/org/testcontainers/spock/TestcontainersSharedContainerIT.groovy ================================================ package org.testcontainers.spock import org.apache.http.client.methods.CloseableHttpResponse import org.apache.http.client.methods.HttpGet import org.apache.http.impl.client.CloseableHttpClient import org.apache.http.impl.client.HttpClientBuilder import org.testcontainers.containers.GenericContainer import spock.lang.Shared import spock.lang.Specification import spock.lang.Stepwise @Stepwise @Testcontainers class TestcontainersSharedContainerIT extends Specification { @Shared GenericContainer genericContainer = new GenericContainer(SpockTestImages.HTTPD_IMAGE) .withExposedPorts(80) @Shared String lastContainerId def "starts accessible docker container"() { given: "a http client" def client = HttpClientBuilder.create().build() lastContainerId = genericContainer.containerId when: "accessing web server" CloseableHttpResponse response = performHttpRequest(client) then: "docker container is running and returns http status code 200" response.statusLine.statusCode == 200 } def "containers keeps on running between features"() { expect: genericContainer.containerId == lastContainerId } private CloseableHttpResponse performHttpRequest(CloseableHttpClient client) { String ip = genericContainer.host String port = genericContainer.getMappedPort(80) def response = client.execute(new HttpGet("http://$ip:$port")) response } } ================================================ FILE: modules/spock/src/test/resources/docker-compose.yml ================================================ version: '2.1' services: whoami: image: emilevauge/whoami ================================================ FILE: modules/spock/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/tidb/build.gradle ================================================ description = "Testcontainers :: JDBC :: TiDB" dependencies { api project(':testcontainers-jdbc') testImplementation project(':testcontainers-jdbc-test') testRuntimeOnly 'com.mysql:mysql-connector-j:9.5.0' compileOnly 'org.jetbrains:annotations:26.0.2-1' } ================================================ FILE: modules/tidb/sql/init_mysql.sql ================================================ CREATE TABLE bar ( foo VARCHAR(255) ); INSERT INTO bar (foo) VALUES ('hello world'); ================================================ FILE: modules/tidb/src/main/java/org/testcontainers/tidb/TiDBContainer.java ================================================ package org.testcontainers.tidb; import org.jetbrains.annotations.NotNull; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; import org.testcontainers.utility.DockerImageName; import java.time.Duration; import java.util.Set; /** * Testcontainers implementation for TiDB. *

* Supported image: {@code pingcap/tidb} *

* Exposed ports: *

    *
  • Database: 4000
  • *
  • HTTP: 10080
  • *
*/ public class TiDBContainer extends JdbcDatabaseContainer { static final String NAME = "tidb"; static final String DOCKER_IMAGE_NAME = "pingcap/tidb"; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(DOCKER_IMAGE_NAME); private static final Integer TIDB_PORT = 4000; private static final int REST_API_PORT = 10080; private String databaseName = "test"; private String username = "root"; private String password = ""; public TiDBContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public TiDBContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); addExposedPorts(TIDB_PORT, REST_API_PORT); waitingFor( new HttpWaitStrategy() .forPath("/status") .forPort(REST_API_PORT) .forStatusCode(200) .withStartupTimeout(Duration.ofMinutes(1)) ); } /** * @return the ports on which to check if the container is ready * @deprecated use {@link #getLivenessCheckPortNumbers()} instead */ @NotNull @Override @Deprecated protected Set getLivenessCheckPorts() { return super.getLivenessCheckPorts(); } @Override public String getDriverClassName() { try { Class.forName("com.mysql.cj.jdbc.Driver"); return "com.mysql.cj.jdbc.Driver"; } catch (ClassNotFoundException e) { return "com.mysql.jdbc.Driver"; } } @Override public String getJdbcUrl() { String additionalUrlParams = constructUrlParameters("?", "&"); return "jdbc:mysql://" + getHost() + ":" + getMappedPort(TIDB_PORT) + "/" + databaseName + additionalUrlParams; } @Override protected String constructUrlForConnection(String queryString) { String url = super.constructUrlForConnection(queryString); if (!url.contains("useSSL=")) { String separator = url.contains("?") ? "&" : "?"; url = url + separator + "useSSL=false"; } if (!url.contains("allowPublicKeyRetrieval=")) { url = url + "&allowPublicKeyRetrieval=true"; } return url; } @Override public String getDatabaseName() { return databaseName; } @Override public String getUsername() { return username; } @Override public String getPassword() { return password; } @Override public String getTestQueryString() { return "SELECT 1"; } @Override public TiDBContainer withDatabaseName(final String databaseName) { throw new UnsupportedOperationException("The TiDB docker image does not currently support this"); } @Override public TiDBContainer withUsername(final String username) { throw new UnsupportedOperationException("The TiDB docker image does not currently support this"); } @Override public TiDBContainer withPassword(final String password) { throw new UnsupportedOperationException("The TiDB docker image does not currently support this"); } } ================================================ FILE: modules/tidb/src/main/java/org/testcontainers/tidb/TiDBContainerProvider.java ================================================ package org.testcontainers.tidb; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.containers.JdbcDatabaseContainerProvider; import org.testcontainers.utility.DockerImageName; /** * Factory for TiDB containers. */ public class TiDBContainerProvider extends JdbcDatabaseContainerProvider { private static final String DEFAULT_TAG = "v6.1.0"; @Override public boolean supports(String databaseType) { return databaseType.equals(TiDBContainer.NAME); } @Override public JdbcDatabaseContainer newInstance() { return newInstance(DEFAULT_TAG); } @Override public JdbcDatabaseContainer newInstance(String tag) { if (tag != null) { return new TiDBContainer(DockerImageName.parse(TiDBContainer.DOCKER_IMAGE_NAME).withTag(tag)); } else { return newInstance(); } } } ================================================ FILE: modules/tidb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider ================================================ org.testcontainers.tidb.TiDBContainerProvider ================================================ FILE: modules/tidb/src/test/java/org/testcontainers/TiDBTestImages.java ================================================ package org.testcontainers; import org.testcontainers.utility.DockerImageName; public class TiDBTestImages { public static final DockerImageName TIDB_IMAGE = DockerImageName.parse("pingcap/tidb:v6.1.0"); } ================================================ FILE: modules/tidb/src/test/java/org/testcontainers/jdbc/tidb/TiDBJDBCDriverTest.java ================================================ package org.testcontainers.jdbc.tidb; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; public class TiDBJDBCDriverTest extends AbstractJDBCDriverTest { public static Iterable data() { return Arrays.asList( new Object[][] { { "jdbc:tc:tidb://hostname/databasename", EnumSet.noneOf(Options.class) } } ); } } ================================================ FILE: modules/tidb/src/test/java/org/testcontainers/tidb/TiDBContainerTest.java ================================================ package org.testcontainers.tidb; import org.junit.jupiter.api.Test; import org.testcontainers.TiDBTestImages; import org.testcontainers.db.AbstractContainerDatabaseTest; import java.sql.ResultSet; import java.sql.SQLException; import static org.assertj.core.api.Assertions.assertThat; class TiDBContainerTest extends AbstractContainerDatabaseTest { @Test void testSimple() throws SQLException { try ( // container { TiDBContainer tidb = new TiDBContainer("pingcap/tidb:v6.1.0") // } ) { tidb.start(); ResultSet resultSet = performQuery(tidb, "SELECT 1"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).isEqualTo(1); assertHasCorrectExposedAndLivenessCheckPorts(tidb); } } @Test void testExplicitInitScript() throws SQLException { try ( TiDBContainer tidb = new TiDBContainer(TiDBTestImages.TIDB_IMAGE).withInitScript("somepath/init_tidb.sql") ) { // TiDB is expected to be compatible with MySQL tidb.start(); ResultSet resultSet = performQuery(tidb, "SELECT foo FROM bar"); String firstColumnValue = resultSet.getString(1); assertThat(firstColumnValue).isEqualTo("hello world"); } } @Test void testWithAdditionalUrlParamInJdbcUrl() { TiDBContainer tidb = new TiDBContainer(TiDBTestImages.TIDB_IMAGE).withUrlParam("sslmode", "disable"); try { tidb.start(); String jdbcUrl = tidb.getJdbcUrl(); assertThat(jdbcUrl).contains("?"); assertThat(jdbcUrl).contains("sslmode=disable"); } finally { tidb.stop(); } } private void assertHasCorrectExposedAndLivenessCheckPorts(TiDBContainer tidb) { Integer tidbPort = 4000; Integer restApiPort = 10080; assertThat(tidb.getExposedPorts()).containsExactlyInAnyOrder(tidbPort, restApiPort); assertThat(tidb.getLivenessCheckPortNumbers()) .containsExactlyInAnyOrder(tidb.getMappedPort(tidbPort), tidb.getMappedPort(restApiPort)); } } ================================================ FILE: modules/tidb/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/tidb/src/test/resources/somepath/init_tidb.sql ================================================ CREATE TABLE bar ( foo VARCHAR(255) ); SELECT "a /* string literal containing comment characters like -- here"; SELECT "a 'quoting' \"scenario ` involving BEGIN keyword\" here"; SELECT * from `bar`; -- What about a line comment containing imbalanced string delimiters? " /* or a block comment containing imbalanced string delimiters? ' " */ INSERT INTO bar (foo) /* ; */ VALUES ('hello world'); ================================================ FILE: modules/timeplus/build.gradle ================================================ description = "Testcontainers :: JDBC :: Timeplus" dependencies { api project(':testcontainers') api project(':testcontainers-jdbc') testImplementation project(':testcontainers-jdbc-test') testRuntimeOnly 'com.timeplus:timeplus-native-jdbc:2.0.10' } ================================================ FILE: modules/timeplus/src/main/java/org/testcontainers/timeplus/TimeplusContainer.java ================================================ package org.testcontainers.timeplus; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import java.time.Duration; import java.util.HashSet; import java.util.Set; /** * Testcontainers implementation for Timeplus. *

* Supported image: {@code timeplus/timeplusd} *

* Exposed ports: *

    *
  • Database: 8463
  • *
  • HTTP: 3218
  • *
*/ public class TimeplusContainer extends JdbcDatabaseContainer { static final String NAME = "timeplus"; static final String DOCKER_IMAGE_NAME = "timeplus/timeplusd"; private static final DockerImageName TIMEPLUS_IMAGE_NAME = DockerImageName.parse(DOCKER_IMAGE_NAME); private static final Integer HTTP_PORT = 3218; private static final Integer NATIVE_PORT = 8463; private static final String DRIVER_CLASS_NAME = "com.timeplus.jdbc.TimeplusDriver"; private static final String JDBC_URL_PREFIX = "jdbc:" + NAME + "://"; private static final String TEST_QUERY = "SELECT 1"; private String databaseName = "default"; private String username = "default"; private String password = ""; public TimeplusContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public TimeplusContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(TIMEPLUS_IMAGE_NAME); addExposedPorts(HTTP_PORT, NATIVE_PORT); waitingFor(Wait.forHttp("/timeplusd/v1/ping").forStatusCode(200).withStartupTimeout(Duration.ofMinutes(1))); } @Override protected void configure() { withEnv("TIMEPLUS_DB", this.databaseName); withEnv("TIMEPLUS_USER", this.username); withEnv("TIMEPLUS_PASSWORD", this.password); } @Override public Set getLivenessCheckPortNumbers() { return new HashSet<>(getMappedPort(HTTP_PORT)); } @Override public String getDriverClassName() { return DRIVER_CLASS_NAME; } @Override public String getJdbcUrl() { return ( JDBC_URL_PREFIX + getHost() + ":" + getMappedPort(NATIVE_PORT) + "/" + this.databaseName + constructUrlParameters("?", "&") ); } @Override public String getUsername() { return this.username; } @Override public String getPassword() { return this.password; } @Override public String getDatabaseName() { return this.databaseName; } @Override public String getTestQueryString() { return TEST_QUERY; } @Override public TimeplusContainer withUsername(String username) { this.username = username; return this; } @Override public TimeplusContainer withPassword(String password) { this.password = password; return this; } @Override public TimeplusContainer withDatabaseName(String databaseName) { this.databaseName = databaseName; return this; } } ================================================ FILE: modules/timeplus/src/main/java/org/testcontainers/timeplus/TimeplusContainerProvider.java ================================================ package org.testcontainers.timeplus; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.containers.JdbcDatabaseContainerProvider; import org.testcontainers.utility.DockerImageName; /** * Factory for Timeplus containers. */ public class TimeplusContainerProvider extends JdbcDatabaseContainerProvider { private static final String DEFAULT_TAG = "2.3.21"; @Override public boolean supports(String databaseType) { return databaseType.equals(TimeplusContainer.NAME); } @Override public JdbcDatabaseContainer newInstance() { return newInstance(DEFAULT_TAG); } @Override public JdbcDatabaseContainer newInstance(String tag) { if (tag != null) { return new TimeplusContainer(DockerImageName.parse(TimeplusContainer.DOCKER_IMAGE_NAME).withTag(tag)); } else { return newInstance(); } } } ================================================ FILE: modules/timeplus/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider ================================================ org.testcontainers.timeplus.TimeplusContainerProvider ================================================ FILE: modules/timeplus/src/test/java/org/testcontainers/TimeplusImages.java ================================================ package org.testcontainers; import org.testcontainers.utility.DockerImageName; public interface TimeplusImages { DockerImageName TIMEPLUS_IMAGE = DockerImageName.parse("timeplus/timeplusd:2.3.21"); } ================================================ FILE: modules/timeplus/src/test/java/org/testcontainers/junit/timeplus/TimeplusJDBCDriverTest.java ================================================ package org.testcontainers.junit.timeplus; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; class TimeplusJDBCDriverTest extends AbstractJDBCDriverTest { public static Iterable data() { return Arrays.asList( new Object[][] { { "jdbc:tc:timeplus:2.3.21://hostname", EnumSet.noneOf(Options.class) } } ); } } ================================================ FILE: modules/timeplus/src/test/java/org/testcontainers/timeplus/TimeplusContainerTest.java ================================================ package org.testcontainers.timeplus; import org.junit.jupiter.api.Test; import org.testcontainers.TimeplusImages; import org.testcontainers.db.AbstractContainerDatabaseTest; import java.sql.ResultSet; import java.sql.SQLException; import static org.assertj.core.api.Assertions.assertThat; class TimeplusContainerTest extends AbstractContainerDatabaseTest { @Test void testSimple() throws SQLException { try ( // container { TimeplusContainer timeplus = new TimeplusContainer("timeplus/timeplusd:2.3.21") // } ) { timeplus.start(); ResultSet resultSet = performQuery(timeplus, "SELECT 1"); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).isEqualTo(1); } } @Test void customCredentialsWithUrlParams() throws SQLException { try ( TimeplusContainer timeplus = new TimeplusContainer(TimeplusImages.TIMEPLUS_IMAGE) .withUsername("system") .withPassword("sys@t+") .withDatabaseName("system") .withUrlParam("interactive_delay", "5") ) { timeplus.start(); ResultSet resultSet = performQuery( timeplus, "SELECT to_int(value) FROM system.settings where name='interactive_delay'" ); int resultSetInt = resultSet.getInt(1); assertThat(resultSetInt).isEqualTo(5); } } } ================================================ FILE: modules/timeplus/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/toxiproxy/build.gradle ================================================ description = "Testcontainers :: Toxiproxy" dependencies { api project(':testcontainers') api 'eu.rekawek.toxiproxy:toxiproxy-java:2.1.11' testImplementation 'redis.clients:jedis:7.1.0' } ================================================ FILE: modules/toxiproxy/src/main/java/org/testcontainers/containers/ToxiproxyContainer.java ================================================ package org.testcontainers.containers; import com.github.dockerjava.api.command.InspectContainerResponse; import eu.rekawek.toxiproxy.Proxy; import eu.rekawek.toxiproxy.ToxiproxyClient; import eu.rekawek.toxiproxy.model.ToxicDirection; import eu.rekawek.toxiproxy.model.ToxicList; import lombok.AccessLevel; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; import org.testcontainers.utility.DockerImageName; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; /** * Testcontainers implementation for Toxiproxy. *

* Supported images: {@code ghcr.io/shopify/toxiproxy}, {@code shopify/toxiproxy} *

* Exposed ports: *

    *
  • HTTP: 8474
  • *
  • Proxied Ports: 8666-8697
  • *
* * @deprecated use {@link org.testcontainers.toxiproxy.ToxiproxyContainer} instead. */ @Deprecated public class ToxiproxyContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("shopify/toxiproxy"); private static final String DEFAULT_TAG = "2.1.0"; private static final DockerImageName GHCR_IMAGE_NAME = DockerImageName.parse("ghcr.io/shopify/toxiproxy"); private static final int TOXIPROXY_CONTROL_PORT = 8474; private static final int FIRST_PROXIED_PORT = 8666; private static final int LAST_PROXIED_PORT = 8666 + 31; private ToxiproxyClient client; private final Map proxies = new HashMap<>(); private final AtomicInteger nextPort = new AtomicInteger(FIRST_PROXIED_PORT); /** * @deprecated use {@link #ToxiproxyContainer(DockerImageName)} instead */ @Deprecated public ToxiproxyContainer() { this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } public ToxiproxyContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public ToxiproxyContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, GHCR_IMAGE_NAME); addExposedPorts(TOXIPROXY_CONTROL_PORT); setWaitStrategy(new HttpWaitStrategy().forPath("/version").forPort(TOXIPROXY_CONTROL_PORT)); // allow up to 32 ports to be proxied (arbitrary value). Here we make the ports exposed; whether or not // Toxiproxy will listen is controlled at runtime using getProxy(...) for (int i = FIRST_PROXIED_PORT; i <= LAST_PROXIED_PORT; i++) { addExposedPort(i); } } @Override protected void containerIsStarted(InspectContainerResponse containerInfo) { client = new ToxiproxyClient(getHost(), getMappedPort(TOXIPROXY_CONTROL_PORT)); } /** * @return Publicly exposed Toxiproxy HTTP API control port. */ public int getControlPort() { return getMappedPort(TOXIPROXY_CONTROL_PORT); } /** * Obtain a {@link ContainerProxy} instance for target container that is managed by Testcontainers. The target * container should be routable from this {@link ToxiproxyContainer} instance (e.g. on the same * Docker {@link Network}). * * @param container target container * @param port port number on the target service that should be proxied * @return a {@link ContainerProxy} instance * @deprecated {@link ToxiproxyContainer} will not build the client. Proxies should be provided manually. */ @Deprecated public ContainerProxy getProxy(GenericContainer container, int port) { return this.getProxy(container.getNetworkAliases().get(0), port); } /** * Obtain a {@link ContainerProxy} instance for a specific hostname and port, which can be for any host * that is routable from this {@link ToxiproxyContainer} instance (e.g. on the same * Docker {@link Network} or on routable from the Docker host). * *

It is expected that {@link ToxiproxyContainer#getProxy(GenericContainer, int)} will be more * useful in most scenarios, but this method is present to allow use of Toxiproxy in front of containers * or external servers that are not managed by Testcontainers.

* * @param hostname hostname of target server to be proxied * @param port port number on the target server that should be proxied * @return a {@link ContainerProxy} instance * @deprecated {@link ToxiproxyContainer} will not build the client. Proxies should be provided manually. */ @Deprecated public ContainerProxy getProxy(String hostname, int port) { String upstream = hostname + ":" + port; return proxies.computeIfAbsent( upstream, __ -> { try { final int toxiPort = nextPort.getAndIncrement(); if (toxiPort > LAST_PROXIED_PORT) { throw new IllegalStateException("Maximum number of proxies exceeded"); } final Proxy proxy = client.createProxy(upstream, "0.0.0.0:" + toxiPort, upstream); final int mappedPort = getMappedPort(toxiPort); return new ContainerProxy(proxy, getHost(), mappedPort, toxiPort); } catch (IOException e) { throw new RuntimeException("Proxy could not be created", e); } } ); } @RequiredArgsConstructor(access = AccessLevel.PROTECTED) @Deprecated public static class ContainerProxy { private static final String CUT_CONNECTION_DOWNSTREAM = "CUT_CONNECTION_DOWNSTREAM"; private static final String CUT_CONNECTION_UPSTREAM = "CUT_CONNECTION_UPSTREAM"; private final Proxy toxi; /** * The IP address that this proxy container may be reached on from the host machine. */ @Getter private final String containerIpAddress; /** * The mapped port of this proxy. This is a port of the host machine. It can be used to * access the Toxiproxy container from the host machine. */ @Getter private final int proxyPort; /** * The original (exposed) port of this proxy. This is a port of the Toxiproxy Docker * container. It can be used to access this container from a different Docker container * on the same network. */ @Getter private final int originalProxyPort; private boolean isCurrentlyCut; public String getName() { return toxi.getName(); } public ToxicList toxics() { return toxi.toxics(); } /** * Cuts the connection by setting bandwidth in both directions to zero. * @param shouldCutConnection true if the connection should be cut, or false if it should be re-enabled */ public void setConnectionCut(boolean shouldCutConnection) { try { if (shouldCutConnection) { toxics().bandwidth(CUT_CONNECTION_DOWNSTREAM, ToxicDirection.DOWNSTREAM, 0); toxics().bandwidth(CUT_CONNECTION_UPSTREAM, ToxicDirection.UPSTREAM, 0); isCurrentlyCut = true; } else if (isCurrentlyCut) { toxics().get(CUT_CONNECTION_DOWNSTREAM).remove(); toxics().get(CUT_CONNECTION_UPSTREAM).remove(); isCurrentlyCut = false; } } catch (IOException e) { throw new RuntimeException("Could not control proxy", e); } } } } ================================================ FILE: modules/toxiproxy/src/main/java/org/testcontainers/toxiproxy/ToxiproxyContainer.java ================================================ package org.testcontainers.toxiproxy; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; import org.testcontainers.utility.DockerImageName; /** * Testcontainers implementation for Toxiproxy. *

* Supported images: {@code ghcr.io/shopify/toxiproxy}, {@code shopify/toxiproxy} *

* Exposed ports: *

    *
  • HTTP: 8474
  • *
  • Proxied Ports: 8666-8697
  • *
*/ public class ToxiproxyContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("shopify/toxiproxy"); private static final DockerImageName GHCR_IMAGE_NAME = DockerImageName.parse("ghcr.io/shopify/toxiproxy"); private static final int TOXIPROXY_CONTROL_PORT = 8474; private static final int FIRST_PROXIED_PORT = 8666; private static final int LAST_PROXIED_PORT = 8666 + 31; public ToxiproxyContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public ToxiproxyContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, GHCR_IMAGE_NAME); addExposedPorts(TOXIPROXY_CONTROL_PORT); setWaitStrategy(new HttpWaitStrategy().forPath("/version").forPort(TOXIPROXY_CONTROL_PORT)); // allow up to 32 ports to be proxied (arbitrary value). Here we make the ports exposed; whether or not // Toxiproxy will listen is controlled at runtime using getProxy(...) for (int i = FIRST_PROXIED_PORT; i <= LAST_PROXIED_PORT; i++) { addExposedPort(i); } } /** * @return Publicly exposed Toxiproxy HTTP API control port. */ public int getControlPort() { return getMappedPort(TOXIPROXY_CONTROL_PORT); } } ================================================ FILE: modules/toxiproxy/src/test/java/org/testcontainers/toxiproxy/ToxiproxyContainerTest.java ================================================ package org.testcontainers.toxiproxy; import eu.rekawek.toxiproxy.Proxy; import eu.rekawek.toxiproxy.ToxiproxyClient; import eu.rekawek.toxiproxy.model.ToxicDirection; import org.junit.jupiter.api.AutoClose; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import redis.clients.jedis.Jedis; import redis.clients.jedis.exceptions.JedisConnectionException; import java.io.IOException; import java.time.Duration; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; public class ToxiproxyContainerTest { private static final Duration JEDIS_TIMEOUT = Duration.ofSeconds(10); // spotless:off // creatingProxy { // Create a common docker network so that containers can communicate @AutoClose public Network network = Network.newNetwork(); // The target container - this could be anything @AutoClose public GenericContainer redis = new GenericContainer<>("redis:6-alpine") .withExposedPorts(6379) .withNetwork(network) .withNetworkAliases("redis"); // Toxiproxy container, which will be used as a TCP proxy @AutoClose public ToxiproxyContainer toxiproxy = new ToxiproxyContainer("ghcr.io/shopify/toxiproxy:2.5.0") .withNetwork(network); // } // spotless:on @BeforeEach public void setUp() { redis.start(); toxiproxy.start(); } @Test public void testDirect() { final Jedis jedis = createJedis(redis.getHost(), redis.getFirstMappedPort()); jedis.set("somekey", "somevalue"); final String s = jedis.get("somekey"); assertThat(s).as("direct access to the container works OK").isEqualTo("somevalue"); } @Test public void testLatencyViaProxy() throws IOException { // obtainProxyObject { final ToxiproxyClient toxiproxyClient = new ToxiproxyClient(toxiproxy.getHost(), toxiproxy.getControlPort()); final Proxy proxy = toxiproxyClient.createProxy("redis", "0.0.0.0:8666", "redis:6379"); // } // obtainProxiedHostAndPortForHostMachine { final String ipAddressViaToxiproxy = toxiproxy.getHost(); final int portViaToxiproxy = toxiproxy.getMappedPort(8666); // } final Jedis jedis = createJedis(ipAddressViaToxiproxy, portViaToxiproxy); jedis.set("somekey", "somevalue"); checkCallWithLatency(jedis, "without interference", 0, 250); // spotless:off // addingLatency { proxy.toxics() .latency("latency", ToxicDirection.DOWNSTREAM, 1_100) .setJitter(100); // from now on the connection latency should be from 1000-1200 ms. // } // spotless:on checkCallWithLatency(jedis, "with interference", 1_000, 1_500); } @Test public void testConnectionCut() throws IOException { final ToxiproxyClient toxiproxyClient = new ToxiproxyClient(toxiproxy.getHost(), toxiproxy.getControlPort()); final Proxy proxy = toxiproxyClient.createProxy("redis", "0.0.0.0:8666", "redis:6379"); final Jedis jedis = createJedis(toxiproxy.getHost(), toxiproxy.getMappedPort(8666)); jedis.set("somekey", "somevalue"); assertThat(jedis.get("somekey")) .as("access to the container works OK before cutting the connection") .isEqualTo("somevalue"); // disableProxy { proxy.toxics().bandwidth("CUT_CONNECTION_DOWNSTREAM", ToxicDirection.DOWNSTREAM, 0); proxy.toxics().bandwidth("CUT_CONNECTION_UPSTREAM", ToxicDirection.UPSTREAM, 0); // for example, expect failure when the connection is cut assertThat( catchThrowable(() -> { jedis.get("somekey"); }) ) .as("calls fail when the connection is cut") .isInstanceOf(JedisConnectionException.class); proxy.toxics().get("CUT_CONNECTION_DOWNSTREAM").remove(); proxy.toxics().get("CUT_CONNECTION_UPSTREAM").remove(); jedis.close(); // and with the connection re-established, expect success assertThat(jedis.get("somekey")) .as("access to the container works OK after re-establishing the connection") .isEqualTo("somevalue"); // } } @Test public void testMultipleProxiesCanBeCreated() throws IOException { try ( GenericContainer secondRedis = new GenericContainer<>("redis:6-alpine") .withExposedPorts(6379) .withNetwork(network) .withNetworkAliases("redis2") ) { secondRedis.start(); final ToxiproxyClient toxiproxyClient = new ToxiproxyClient( toxiproxy.getHost(), toxiproxy.getControlPort() ); final Proxy firstProxy = toxiproxyClient.createProxy("redis1", "0.0.0.0:8666", "redis:6379"); toxiproxyClient.createProxy("redis2", "0.0.0.0:8667", "redis2:6379"); final Jedis firstJedis = createJedis(toxiproxy.getHost(), toxiproxy.getMappedPort(8666)); final Jedis secondJedis = createJedis(toxiproxy.getHost(), toxiproxy.getMappedPort(8667)); firstJedis.set("somekey", "somevalue"); secondJedis.set("somekey", "somevalue"); firstProxy.toxics().bandwidth("CUT_CONNECTION_DOWNSTREAM", ToxicDirection.DOWNSTREAM, 0); firstProxy.toxics().bandwidth("CUT_CONNECTION_UPSTREAM", ToxicDirection.UPSTREAM, 0); assertThat( catchThrowable(() -> { firstJedis.get("somekey"); }) ) .as("calls fail when the connection is cut, for only the relevant proxy") .isInstanceOf(JedisConnectionException.class); assertThat(secondJedis.get("somekey")).as("access via a different proxy is OK").isEqualTo("somevalue"); } } private void checkCallWithLatency( Jedis jedis, final String description, int expectedMinLatency, long expectedMaxLatency ) { final long start = System.nanoTime(); String s = jedis.get("somekey"); final long end = System.nanoTime(); final long duration = TimeUnit.NANOSECONDS.toMillis(end - start); assertThat(s).as(String.format("access to the container %s works OK", description)).isEqualTo("somevalue"); assertThat(duration >= expectedMinLatency) .as(String.format("%s there is at least %dms latency", description, expectedMinLatency)) .isTrue(); assertThat(duration < expectedMaxLatency) .as(String.format("%s there is no more than %dms latency", description, expectedMaxLatency)) .isTrue(); } private static Jedis createJedis(String host, int port) { return new Jedis(host, port, Math.toIntExact(JEDIS_TIMEOUT.toMillis())); } } ================================================ FILE: modules/toxiproxy/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/trino/build.gradle ================================================ description = "Testcontainers :: JDBC :: Trino" dependencies { api project(':testcontainers-jdbc') testImplementation project(':testcontainers-jdbc-test') testRuntimeOnly 'io.trino:trino-jdbc:478' compileOnly 'org.jetbrains:annotations:26.0.2-1' } ================================================ FILE: modules/trino/src/main/java/org/testcontainers/containers/TrinoContainer.java ================================================ package org.testcontainers.containers; import com.google.common.base.Strings; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.VisibleForTesting; import org.testcontainers.utility.DockerImageName; import java.sql.Connection; import java.sql.SQLException; import java.util.Set; /** * Testcontainers implementation for TrinoDB. *

* Supported image: {@code trinodb/trino} *

* Exposed ports: 8080 * * @deprecated use {@link org.testcontainers.trino.TrinoContainer} instead. */ @Deprecated public class TrinoContainer extends JdbcDatabaseContainer { static final String NAME = "trino"; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("trinodb/trino"); static final String IMAGE = "trinodb/trino"; @VisibleForTesting static final String DEFAULT_TAG = "352"; private static final int TRINO_PORT = 8080; private String username = "test"; private String catalog = null; public TrinoContainer(final String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public TrinoContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); addExposedPort(TRINO_PORT); } /** * @return the ports on which to check if the container is ready * @deprecated use {@link #getLivenessCheckPortNumbers()} instead */ @NotNull @Override @Deprecated protected Set getLivenessCheckPorts() { return super.getLivenessCheckPorts(); } @Override public String getDriverClassName() { return "io.trino.jdbc.TrinoDriver"; } @Override public String getJdbcUrl() { return String.format( "jdbc:trino://%s:%s/%s", getHost(), getMappedPort(TRINO_PORT), Strings.nullToEmpty(catalog) ); } @Override public String getUsername() { return username; } @Override public String getPassword() { return ""; } @Override public String getDatabaseName() { return catalog; } @Override public String getTestQueryString() { return "SELECT count(*) FROM tpch.tiny.nation"; } @Override public TrinoContainer withUsername(final String username) { this.username = username; return this; } @Override public TrinoContainer withDatabaseName(String dbName) { this.catalog = dbName; return this; } public Connection createConnection() throws SQLException, NoDriverFoundException { return createConnection(""); } } ================================================ FILE: modules/trino/src/main/java/org/testcontainers/containers/TrinoContainerProvider.java ================================================ package org.testcontainers.containers; import org.testcontainers.utility.DockerImageName; /** * Factory for Trino containers. */ public class TrinoContainerProvider extends JdbcDatabaseContainerProvider { @Override public boolean supports(String databaseType) { return databaseType.equals(TrinoContainer.NAME); } @Override public JdbcDatabaseContainer newInstance() { return newInstance(TrinoContainer.DEFAULT_TAG); } @Override public JdbcDatabaseContainer newInstance(String tag) { return new TrinoContainer(DockerImageName.parse(TrinoContainer.IMAGE).withTag(tag)); } } ================================================ FILE: modules/trino/src/main/java/org/testcontainers/trino/TrinoContainer.java ================================================ package org.testcontainers.trino; import com.google.common.base.Strings; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.VisibleForTesting; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.utility.DockerImageName; import java.sql.Connection; import java.sql.SQLException; import java.util.Set; /** * Testcontainers implementation for TrinoDB. *

* Supported image: {@code trinodb/trino} *

* Exposed ports: 8080 */ public class TrinoContainer extends JdbcDatabaseContainer { static final String NAME = "trino"; private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("trinodb/trino"); static final String IMAGE = "trinodb/trino"; @VisibleForTesting static final String DEFAULT_TAG = "352"; private static final int TRINO_PORT = 8080; private String username = "test"; private String catalog = null; public TrinoContainer(final String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public TrinoContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); addExposedPort(TRINO_PORT); } /** * @return the ports on which to check if the container is ready * @deprecated use {@link #getLivenessCheckPortNumbers()} instead */ @NotNull @Override @Deprecated protected Set getLivenessCheckPorts() { return super.getLivenessCheckPorts(); } @Override public String getDriverClassName() { return "io.trino.jdbc.TrinoDriver"; } @Override public String getJdbcUrl() { return String.format( "jdbc:trino://%s:%s/%s", getHost(), getMappedPort(TRINO_PORT), Strings.nullToEmpty(catalog) ); } @Override public String getUsername() { return username; } @Override public String getPassword() { return ""; } @Override public String getDatabaseName() { return catalog; } @Override public String getTestQueryString() { return "SELECT count(*) FROM tpch.tiny.nation"; } @Override public TrinoContainer withUsername(final String username) { this.username = username; return this; } @Override public TrinoContainer withDatabaseName(String dbName) { this.catalog = dbName; return this; } public Connection createConnection() throws SQLException, NoDriverFoundException { return createConnection(""); } } ================================================ FILE: modules/trino/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider ================================================ org.testcontainers.containers.TrinoContainerProvider ================================================ FILE: modules/trino/src/test/java/org/testcontainers/TrinoTestImages.java ================================================ package org.testcontainers; import org.testcontainers.utility.DockerImageName; public interface TrinoTestImages { DockerImageName TRINO_TEST_IMAGE = DockerImageName.parse("trinodb/trino:352"); DockerImageName TRINO_PREVIOUS_VERSION_TEST_IMAGE = DockerImageName.parse("trinodb/trino:351"); } ================================================ FILE: modules/trino/src/test/java/org/testcontainers/jdbc/trino/TrinoJDBCDriverTest.java ================================================ package org.testcontainers.jdbc.trino; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; class TrinoJDBCDriverTest extends AbstractJDBCDriverTest { public static Iterable data() { return Arrays.asList( new Object[][] { // { "jdbc:tc:trino:352://hostname/", EnumSet.of(Options.PmdKnownBroken) }, } ); } } ================================================ FILE: modules/trino/src/test/java/org/testcontainers/trino/TrinoContainerTest.java ================================================ package org.testcontainers.trino; import org.junit.jupiter.api.Test; import org.testcontainers.TrinoTestImages; import java.sql.Connection; import java.sql.ResultSet; import java.sql.Statement; import static org.assertj.core.api.Assertions.assertThat; class TrinoContainerTest { @Test void testSimple() throws Exception { try ( // container { TrinoContainer trino = new TrinoContainer("trinodb/trino:352") // } ) { trino.start(); try ( Connection connection = trino.createConnection(); Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery("SELECT DISTINCT node_version FROM system.runtime.nodes") ) { assertThat(resultSet.next()).as("results").isTrue(); assertThat(resultSet.getString("node_version")).as("Trino version").isEqualTo("352"); assertContainerHasCorrectExposedAndLivenessCheckPorts(trino); } } } @Test void testSpecificVersion() throws Exception { try (TrinoContainer trino = new TrinoContainer(TrinoTestImages.TRINO_PREVIOUS_VERSION_TEST_IMAGE)) { trino.start(); try ( Connection connection = trino.createConnection(); Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery("SELECT DISTINCT node_version FROM system.runtime.nodes") ) { assertThat(resultSet.next()).as("results").isTrue(); assertThat(resultSet.getString("node_version")) .as("Trino version") .isEqualTo(TrinoTestImages.TRINO_PREVIOUS_VERSION_TEST_IMAGE.getVersionPart()); } } } @Test void testInitScript() throws Exception { try (TrinoContainer trino = new TrinoContainer(TrinoTestImages.TRINO_TEST_IMAGE)) { trino.withInitScript("initial.sql"); trino.start(); try ( Connection connection = trino.createConnection(); Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery("SELECT a FROM memory.default.test_table") ) { assertThat(resultSet.next()).as("results").isTrue(); assertThat(resultSet.getObject("a")).as("Value").isEqualTo(12345678909324L); assertThat(resultSet.next()).as("results").isFalse(); } } } private void assertContainerHasCorrectExposedAndLivenessCheckPorts(TrinoContainer trino) { assertThat(trino.getExposedPorts()).containsExactly(8080); assertThat(trino.getLivenessCheckPortNumbers()).containsExactly(trino.getMappedPort(8080)); } } ================================================ FILE: modules/trino/src/test/resources/initial.sql ================================================ CREATE TABLE memory.default.test_table(a bigint); INSERT INTO memory.default.test_table(a) VALUES (12345678909324); ================================================ FILE: modules/trino/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/typesense/build.gradle ================================================ description = "Testcontainers :: Typesense" dependencies { api project(':testcontainers') testImplementation 'org.typesense:typesense-java:2.0.0' } ================================================ FILE: modules/typesense/src/main/java/org/testcontainers/typesense/TypesenseContainer.java ================================================ 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; } } ================================================ FILE: modules/typesense/src/test/java/org/testcontainers/typesense/TypesenseContainerTest.java ================================================ package org.testcontainers.typesense; import org.junit.jupiter.api.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; class TypesenseContainerTest { @Test 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 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); } } } ================================================ FILE: modules/typesense/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/vault/AUTHORS ================================================ Michael Oswald ================================================ FILE: modules/vault/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2017 Capital One Services, LLC and other authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: modules/vault/build.gradle ================================================ description = "Testcontainers :: Vault" dependencies { api project(':testcontainers') testImplementation 'com.bettercloud:vault-java-driver:5.1.0' testImplementation 'io.rest-assured:rest-assured:5.5.6' } ================================================ FILE: modules/vault/src/main/java/org/testcontainers/vault/VaultContainer.java ================================================ package org.testcontainers.vault; import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.api.model.Capability; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; import java.util.stream.Collectors; /** * Testcontainers implementation for Vault. *

* Supported image: {@code hashicorp/vault}, {@code vault} *

* Exposure ports: 8200 */ public class VaultContainer> extends GenericContainer { private static final DockerImageName DEFAULT_OLD_IMAGE_NAME = DockerImageName.parse("vault"); private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("hashicorp/vault"); private static final String DEFAULT_TAG = "1.1.3"; private static final int VAULT_PORT = 8200; private Map> secretsMap = new HashMap<>(); private List initCommands = new ArrayList<>(); private int port = VAULT_PORT; /** * @deprecated use {@link #VaultContainer(DockerImageName)} instead */ @Deprecated public VaultContainer() { this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } public VaultContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public VaultContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_OLD_IMAGE_NAME, DEFAULT_IMAGE_NAME); // Use the vault healthcheck endpoint to check for readiness, per https://www.vaultproject.io/api/system/health.html setWaitStrategy(Wait.forHttp("/v1/sys/health").forStatusCode(200)); withCreateContainerCmdModifier(cmd -> cmd.withCapAdd(Capability.IPC_LOCK)); withEnv("VAULT_ADDR", "http://0.0.0.0:" + port); withExposedPorts(port); } public String getHttpHostAddress() { return String.format("http://%s:%s", getHost(), getMappedPort(port)); } @Override protected void containerIsStarted(InspectContainerResponse containerInfo) { addSecrets(); runInitCommands(); } private void addSecrets() { if (!secretsMap.isEmpty()) { try { this.execInContainer(buildExecCommand(secretsMap)).getStdout().contains("Success"); } catch (IOException | InterruptedException e) { logger() .error( "Failed to add these secrets {} into Vault via exec command. Exception message: {}", secretsMap, e.getMessage() ); } } } private String[] buildExecCommand(Map> map) { StringBuilder stringBuilder = new StringBuilder(); map.forEach((path, secrets) -> { stringBuilder.append(" && vault kv put " + path); secrets.forEach(item -> stringBuilder.append(" " + item)); }); return new String[] { "/bin/sh", "-c", stringBuilder.toString().substring(4) }; } private void runInitCommands() { if (!initCommands.isEmpty()) { String commands = initCommands .stream() .map(command -> "vault " + command) .collect(Collectors.joining(" && ")); try { ExecResult execResult = this.execInContainer(new String[] { "/bin/sh", "-c", commands }); if (execResult.getExitCode() != 0) { logger() .error( "Failed to execute these init commands {}. Exit code {}. Stdout {}. Stderr {}", initCommands, execResult.getExitCode(), execResult.getStdout(), execResult.getStderr() ); } } catch (IOException | InterruptedException e) { logger() .error( "Failed to execute these init commands {}. Exception message: {}", initCommands, e.getMessage() ); } } } /** * Sets the Vault root token for the container so application tests can source secrets using the token * * @param token the root token value to set for Vault. * @return this */ public SELF withVaultToken(String token) { withEnv("VAULT_DEV_ROOT_TOKEN_ID", token); withEnv("VAULT_TOKEN", token); return self(); } /** * Sets the Vault port in the container as well as the port bindings for the host to reach the container over HTTP. * * @param port the port number you want to have the Vault container listen on for tests. * @return this * @deprecated the exposed port will be randomized automatically. As calling this method provides no additional value, you are recommended to remove the call. getFirstMappedPort() may be used to obtain the listening vault port. */ @Deprecated public SELF withVaultPort(int port) { this.port = port; return self(); } /** * Sets the logging level for the Vault server in the container. * Logs can be consumed through {@link #withLogConsumer(Consumer)}. * * @param level the logging level to set for Vault. * @return this * @deprecated use {@link #withEnv(String, String)} instead */ @Deprecated public SELF withLogLevel(VaultLogLevel level) { return withEnv("VAULT_LOG_LEVEL", level.config); } /** * Pre-loads secrets into Vault container. User may specify one or more secrets and all will be added to each path * that is specified. Thus this can be called more than once for multiple paths to be added to Vault. *

* The secrets are added to vault directly after the container is up via the * {@link #addSecrets() addSecrets}, called from {@link #containerIsStarted(InspectContainerResponse) containerIsStarted} * * @param path specific Vault path to store specified secrets * @param firstSecret first secret to add to specified path * @param remainingSecrets var args list of secrets to add to specified path * @return this * @deprecated use {@link #withInitCommand(String...)} instead */ @Deprecated public SELF withSecretInVault(String path, String firstSecret, String... remainingSecrets) { List list = new ArrayList<>(); list.add(firstSecret); for (String secret : remainingSecrets) { list.add(secret); } if (secretsMap.containsKey(path)) { list.addAll(list); } secretsMap.putIfAbsent(path, list); return self(); } /** * Run initialization commands using the vault cli. *

* Useful for enabling more secret engines like: *

     *     .withInitCommand("secrets enable pki")
     *     .withInitCommand("secrets enable transit")
     * 
* @param commands The commands to send to the vault cli * @return this */ public SELF withInitCommand(String... commands) { initCommands.addAll(Arrays.asList(commands)); return self(); } } ================================================ FILE: modules/vault/src/main/java/org/testcontainers/vault/VaultLogLevel.java ================================================ package org.testcontainers.vault; /** * Vault preset of logging levels. */ public enum VaultLogLevel { Trace("trace"), Debug("debug"), Info("info"), Warn("warn"), Error("err"); public final String config; VaultLogLevel(String config) { this.config = config; } } ================================================ FILE: modules/vault/src/test/java/org/testcontainers/vault/VaultClientTest.java ================================================ package org.testcontainers.vault; import com.bettercloud.vault.Vault; import com.bettercloud.vault.VaultConfig; import com.bettercloud.vault.VaultException; import com.bettercloud.vault.response.LogicalResponse; import org.junit.jupiter.api.Test; import java.util.HashMap; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; class VaultClientTest { private static final String VAULT_TOKEN = "my-root-token"; @Test void writeAndReadMultipleValues() throws VaultException { try (VaultContainer vaultContainer = new VaultContainer<>("vault:1.1.3").withVaultToken(VAULT_TOKEN)) { vaultContainer.start(); final VaultConfig config = new VaultConfig() .address("http://" + vaultContainer.getHost() + ":" + vaultContainer.getFirstMappedPort()) .token(VAULT_TOKEN) .build(); final Vault vault = new Vault(config); final Map secrets = new HashMap<>(); secrets.put("value", "world"); secrets.put("other_value", "another world"); // Write operation final LogicalResponse writeResponse = vault.logical().write("secret/hello", secrets); assertThat(writeResponse.getRestResponse().getStatus()).isEqualTo(200); // Read operation final Map value = vault.logical().read("secret/hello").getData(); assertThat(value).containsEntry("value", "world").containsEntry("other_value", "another world"); } } } ================================================ FILE: modules/vault/src/test/java/org/testcontainers/vault/VaultContainerTest.java ================================================ package org.testcontainers.vault; import com.bettercloud.vault.Vault; import com.bettercloud.vault.VaultConfig; import com.bettercloud.vault.response.LogicalResponse; import io.restassured.response.Response; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import java.util.HashMap; import java.util.Map; import static io.restassured.RestAssured.given; import static org.assertj.core.api.Assertions.assertThat; /** * This test shows the pattern to use the VaultContainer @ClassRule for a junit test. It also has tests that ensure * the secrets were added correctly by reading from Vault with the CLI, over HTTP and over Client Library. */ class VaultContainerTest { private static final String VAULT_TOKEN = "my-root-token"; // vaultContainer { public static VaultContainer vaultContainer = new VaultContainer<>("hashicorp/vault:1.13") .withVaultToken(VAULT_TOKEN) .withInitCommand( "secrets enable transit", "write -f transit/keys/my-key", "kv put secret/testing1 top_secret=password123", "kv put secret/testing2 secret_one=password1 secret_two=password2 secret_three=password3 secret_three=password3 secret_four=password4" ); // } @BeforeAll static void setUp() { vaultContainer.start(); } @AfterAll static void tearDown() { vaultContainer.stop(); } @Test void readFirstSecretPathWithCli() throws Exception { GenericContainer.ExecResult result = vaultContainer.execInContainer( "vault", "kv", "get", "-format=json", "secret/testing1" ); assertThat(result.getStdout()).contains("password123"); } @Test void readSecondSecretPathWithCli() throws Exception { GenericContainer.ExecResult result = vaultContainer.execInContainer( "vault", "kv", "get", "-format=json", "secret/testing2" ); final String output = result.getStdout().replaceAll("\\r?\\n", ""); System.out.println("output = " + output); assertThat(output).contains("password1"); assertThat(output).contains("password2"); assertThat(output).contains("password3"); assertThat(output).contains("password4"); } @Test void readFirstSecretPathOverHttpApi() { Response response = given() .header("X-Vault-Token", VAULT_TOKEN) .when() .get(vaultContainer.getHttpHostAddress() + "/v1/secret/data/testing1") .thenReturn(); assertThat(response.body().jsonPath().getString("data.data.top_secret")).isEqualTo("password123"); } @Test void readSecondSecretPathOverHttpApi() throws InterruptedException { Response response = given() .header("X-Vault-Token", VAULT_TOKEN) .when() .get(vaultContainer.getHttpHostAddress() + "/v1/secret/data/testing2") .andReturn(); assertThat(response.body().jsonPath().getString("data.data.secret_one")).contains("password1"); assertThat(response.body().jsonPath().getString("data.data.secret_two")).contains("password2"); assertThat(response.body().jsonPath().getList("data.data.secret_three")).contains("password3"); assertThat(response.body().jsonPath().getString("data.data.secret_four")).contains("password4"); } @Test void readTransitKeyOverHttpApi() throws InterruptedException { Response response = given() .header("X-Vault-Token", VAULT_TOKEN) .when() .get(vaultContainer.getHttpHostAddress() + "/v1/transit/keys/my-key") .thenReturn(); assertThat(response.body().jsonPath().getString("data.name")).isEqualTo("my-key"); } @Test // readWithLibrary { void readFirstSecretPathOverClientLibrary() throws Exception { final VaultConfig config = new VaultConfig() .address(vaultContainer.getHttpHostAddress()) .token(VAULT_TOKEN) .build(); final Vault vault = new Vault(config); final Map value = vault.logical().read("secret/testing1").getData(); assertThat(value).containsEntry("top_secret", "password123"); } // } @Test void readSecondSecretPathOverClientLibrary() throws Exception { final VaultConfig config = new VaultConfig() .address(vaultContainer.getHttpHostAddress()) .token(VAULT_TOKEN) .build(); final Vault vault = new Vault(config); final Map value = vault.logical().read("secret/testing2").getData(); assertThat(value) .containsEntry("secret_one", "password1") .containsEntry("secret_two", "password2") .containsEntry("secret_three", "[\"password3\",\"password3\"]") .containsEntry("secret_four", "password4"); } @Test void writeSecretOverClientLibrary() throws Exception { final VaultConfig config = new VaultConfig() .address(vaultContainer.getHttpHostAddress()) .token(VAULT_TOKEN) .build(); final Vault vault = new Vault(config); final Map secrets = new HashMap<>(); secrets.put("value", "world"); secrets.put("other_value", "another world"); // Write operation final LogicalResponse writeResponse = vault.logical().write("secret/hello", secrets); assertThat(writeResponse.getRestResponse().getStatus()).isEqualTo(200); // Read operation final Map value = vault.logical().read("secret/hello").getData(); assertThat(value).containsEntry("value", "world").containsEntry("other_value", "another world"); } } ================================================ FILE: modules/vault/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/weaviate/build.gradle ================================================ description = "Testcontainers :: Weaviate" dependencies { api project(':testcontainers') testImplementation 'io.weaviate:client:5.5.0' } ================================================ FILE: modules/weaviate/src/main/java/org/testcontainers/weaviate/WeaviateContainer.java ================================================ package org.testcontainers.weaviate; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; /** * Testcontainers implementation of Weaviate. *

* Supported images: {@code cr.weaviate.io/semitechnologies/weaviate}, {@code semitechnologies/weaviate} *

* Exposed ports: *

    *
  • HTTP: 8080
  • *
  • gRPC: 50051
  • *
*/ public class WeaviateContainer extends GenericContainer { private static final DockerImageName DEFAULT_WEAVIATE_IMAGE = DockerImageName.parse( "cr.weaviate.io/semitechnologies/weaviate" ); private static final DockerImageName DOCKER_HUB_WEAVIATE_IMAGE = DockerImageName.parse("semitechnologies/weaviate"); public WeaviateContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } public WeaviateContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_WEAVIATE_IMAGE, DOCKER_HUB_WEAVIATE_IMAGE); withExposedPorts(8080, 50051); withEnv("AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED", "true"); withEnv("PERSISTENCE_DATA_PATH", "/var/lib/weaviate"); waitingFor(Wait.forHttp("/v1/.well-known/ready").forPort(8080).forStatusCode(200)); } public String getHttpHostAddress() { return getHost() + ":" + getMappedPort(8080); } public String getGrpcHostAddress() { return getHost() + ":" + getMappedPort(50051); } } ================================================ FILE: modules/weaviate/src/test/java/org/testcontainers/weaviate/WeaviateContainerTest.java ================================================ package org.testcontainers.weaviate; import io.weaviate.client.Config; import io.weaviate.client.WeaviateClient; import io.weaviate.client.base.Result; import io.weaviate.client.v1.misc.model.Meta; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; class WeaviateContainerTest { @Test void testWeaviate() { try ( // container { WeaviateContainer weaviate = new WeaviateContainer("cr.weaviate.io/semitechnologies/weaviate:1.29.0") // } ) { weaviate.start(); Config config = new Config("http", weaviate.getHttpHostAddress()); config.setGRPCHost(weaviate.getGrpcHostAddress()); WeaviateClient client = new WeaviateClient(config); Result meta = client.misc().metaGetter().run(); assertThat(meta.getResult().getVersion()).isEqualTo("1.29.0"); } } @Test void testWeaviateWithModules() { List enableModules = Arrays.asList( "backup-filesystem", "text2vec-openai", "text2vec-cohere", "text2vec-huggingface", "generative-openai" ); Map env = new HashMap<>(); env.put("ENABLE_MODULES", String.join(",", enableModules)); env.put("BACKUP_FILESYSTEM_PATH", "/tmp/backups"); try (WeaviateContainer weaviate = new WeaviateContainer("semitechnologies/weaviate:1.29.0").withEnv(env)) { weaviate.start(); Config config = new Config("http", weaviate.getHttpHostAddress()); config.setGRPCHost(weaviate.getGrpcHostAddress()); WeaviateClient client = new WeaviateClient(config); Result meta = client.misc().metaGetter().run(); assertThat(meta.getResult().getVersion()).isEqualTo("1.29.0"); Object modules = meta.getResult().getModules(); assertThat(modules) .isNotNull() .asInstanceOf(InstanceOfAssertFactories.map(String.class, Object.class)) .extracting(Map::keySet) .satisfies(keys -> { assertThat(keys.size()).isEqualTo(enableModules.size()); keys.forEach(key -> assertThat(enableModules.contains(key)).isTrue()); }); } } } ================================================ FILE: modules/weaviate/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: modules/yugabytedb/build.gradle ================================================ description = "Testcontainers :: JDBC :: YugabyteDB" dependencies { api project(':testcontainers-jdbc') testImplementation project(':testcontainers-jdbc-test') // YCQL driver testImplementation 'com.yugabyte:java-driver-core:4.19.0-yb-1' // YSQL driver testRuntimeOnly 'com.yugabyte:jdbc-yugabytedb:42.7.3-yb-4' } ================================================ FILE: modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteDBYCQLContainer.java ================================================ package org.testcontainers.containers; import com.github.dockerjava.api.command.InspectContainerResponse; import org.testcontainers.containers.delegate.YugabyteDBYCQLDelegate; import org.testcontainers.containers.strategy.YugabyteDBYCQLWaitStrategy; import org.testcontainers.ext.ScriptUtils; import org.testcontainers.utility.DockerImageName; import java.net.InetSocketAddress; import java.time.Duration; import java.util.Collections; import java.util.Set; /** * Testcontainers implementation for YugabyteDB YCQL API. *

* Supported image: {@code yugabytedb/yugabyte} *

* Exposed ports: *

    *
  • YCQL: 5433
  • *
  • Master dashboard: 7000
  • *
  • Tserver dashboard: 9000
  • *
* * @see YCQL API */ public class YugabyteDBYCQLContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("yugabytedb/yugabyte"); private static final Integer YCQL_PORT = 9042; private static final Integer MASTER_DASHBOARD_PORT = 7000; private static final Integer TSERVER_DASHBOARD_PORT = 9000; private static final String ENTRYPOINT = "bin/yugabyted start --background=false"; private static final String LOCAL_DC = "datacenter1"; private String keyspace; private String username; private String password; private String initScript; /** * @param imageName image name */ public YugabyteDBYCQLContainer(final String imageName) { this(DockerImageName.parse(imageName)); } /** * @param imageName image name */ public YugabyteDBYCQLContainer(final DockerImageName imageName) { super(imageName); imageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); withExposedPorts(YCQL_PORT, MASTER_DASHBOARD_PORT, TSERVER_DASHBOARD_PORT); waitingFor(new YugabyteDBYCQLWaitStrategy(this).withStartupTimeout(Duration.ofSeconds(60))); withCommand(ENTRYPOINT); } @Override public Set getLivenessCheckPortNumbers() { return Collections.singleton(getMappedPort(YCQL_PORT)); } /** * Configures the environment variables. Setting up these variables would create the * custom objects. Setting {@link #withKeyspaceName(String)}, * {@link #withUsername(String)}, {@link #withPassword(String)} these parameters will * initialize the database with those custom values */ @Override protected void configure() { addEnv("YCQL_KEYSPACE", keyspace); addEnv("YCQL_USER", username); addEnv("YCQL_PASSWORD", password); } /** * @param initScript path of the initialization script file * @return {@link YugabyteDBYCQLContainer} instance */ public YugabyteDBYCQLContainer withInitScript(String initScript) { this.initScript = initScript; return this; } /** * Setting this would create the keyspace * @param keyspace keyspace * @return {@link YugabyteDBYCQLContainer} instance */ public YugabyteDBYCQLContainer withKeyspaceName(final String keyspace) { this.keyspace = keyspace; return this; } /** * Setting this would create the custom user role * @param username user name * @return {@link YugabyteDBYCQLContainer} instance */ public YugabyteDBYCQLContainer withUsername(final String username) { this.username = username; return this; } /** * Setting this along with {@link #withUsername(String)} would enable authentication * @param password password * @return {@link YugabyteDBYCQLContainer} instance */ public YugabyteDBYCQLContainer withPassword(final String password) { this.password = password; return this; } /** * Executes the initialization script * @param containerInfo containerInfo */ @Override protected void containerIsStarted(InspectContainerResponse containerInfo) { if (this.initScript != null) { ScriptUtils.runInitScript(new YugabyteDBYCQLDelegate(this), initScript); } } /** * Returns a {@link InetSocketAddress} representation of YCQL's contact point info * @return contactpoint */ public InetSocketAddress getContactPoint() { return new InetSocketAddress(getHost(), getMappedPort(YCQL_PORT)); } /** * Returns the local datacenter name * @return localdc name */ public String getLocalDc() { return LOCAL_DC; } /** * Username getter method * @return username */ public String getUsername() { return this.username; } /** * Password getter method * @return password */ public String getPassword() { return this.password; } /** * Keyspace getter method * @return keyspace */ public String getKeyspace() { return this.keyspace; } } ================================================ FILE: modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteDBYSQLContainer.java ================================================ package org.testcontainers.containers; import org.testcontainers.containers.strategy.YugabyteDBYSQLWaitStrategy; import org.testcontainers.utility.DockerImageName; import java.time.Duration; import java.util.Collections; import java.util.Set; /** * Testcontainers implementation for YugabyteDB YSQL API. *

* Supported image: {@code yugabytedb/yugabyte} *

* Exposed ports: *

    *
  • YSQL: 5433
  • *
  • Master dashboard: 7000
  • *
  • Tserver dashboard: 9000
  • *
* * @see YSQL API */ public class YugabyteDBYSQLContainer extends JdbcDatabaseContainer { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("yugabytedb/yugabyte"); private static final Integer YSQL_PORT = 5433; private static final Integer MASTER_DASHBOARD_PORT = 7000; private static final Integer TSERVER_DASHBOARD_PORT = 9000; private static final String JDBC_DRIVER_CLASS = "com.yugabyte.Driver"; private static final String JDBC_CONNECT_PREFIX = "jdbc:yugabytedb"; private static final String ENTRYPOINT = "bin/yugabyted start --background=false"; private String database = "yugabyte"; private String username = "yugabyte"; private String password = "yugabyte"; /** * @param imageName image name */ public YugabyteDBYSQLContainer(final String imageName) { this(DockerImageName.parse(imageName)); } /** * @param imageName image name */ public YugabyteDBYSQLContainer(final DockerImageName imageName) { super(imageName); imageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); withExposedPorts(YSQL_PORT, MASTER_DASHBOARD_PORT, TSERVER_DASHBOARD_PORT); waitingFor(new YugabyteDBYSQLWaitStrategy(this).withStartupTimeout(Duration.ofSeconds(60))); withCommand(ENTRYPOINT); } @Override public Set getLivenessCheckPortNumbers() { return Collections.singleton(getMappedPort(YSQL_PORT)); } /** * Configures the environment variables. Setting up these variables would create the * custom objects. Setting {@link #withDatabaseName(String)}, * {@link #withUsername(String)}, {@link #withPassword(String)} these parameters will * initialize the database with those custom values */ @Override protected void configure() { addEnv("YSQL_DB", database); addEnv("YSQL_USER", username); addEnv("YSQL_PASSWORD", password); } @Override public String getDriverClassName() { return JDBC_DRIVER_CLASS; } @Override public String getJdbcUrl() { return ( JDBC_CONNECT_PREFIX + "://" + getHost() + ":" + getMappedPort(YSQL_PORT) + "/" + database + constructUrlParameters("?", "&") ); } @Override public String getDatabaseName() { return database; } @Override public String getUsername() { return username; } @Override public String getPassword() { return password; } @Override public String getTestQueryString() { return "SELECT 1"; } /** * Setting this would create the keyspace * @param database database name * @return {@link YugabyteDBYSQLContainer} instance */ @Override public YugabyteDBYSQLContainer withDatabaseName(final String database) { this.database = database; return this; } /** * Setting this would create the custom user role * @param username user name * @return {@link YugabyteDBYSQLContainer} instance */ @Override public YugabyteDBYSQLContainer withUsername(final String username) { this.username = username; return this; } /** * Setting this along with {@link #withUsername(String)} would enable authentication * @param password password * @return {@link YugabyteDBYSQLContainer} instance */ @Override public YugabyteDBYSQLContainer withPassword(final String password) { this.password = password; return this; } @Override protected void waitUntilContainerStarted() { getWaitStrategy().waitUntilReady(this); } } ================================================ FILE: modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteDBYSQLContainerProvider.java ================================================ package org.testcontainers.containers; import org.testcontainers.jdbc.ConnectionUrl; import org.testcontainers.utility.DockerImageName; /** * YugabyteDB YSQL (Structured Query Language) JDBC container provider */ public class YugabyteDBYSQLContainerProvider extends JdbcDatabaseContainerProvider { private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("yugabytedb/yugabyte"); private static final String DEFAULT_TAG = "2.14.4.0-b26"; private static final String NAME = "yugabyte"; private static final String USER_PARAM = "user"; private static final String PASSWORD_PARAM = "password"; @Override public boolean supports(String databaseType) { return databaseType.equals(NAME); } @Override public JdbcDatabaseContainer newInstance() { return newInstance(DEFAULT_TAG); } @Override public JdbcDatabaseContainer newInstance(String tag) { return new YugabyteDBYSQLContainer(DEFAULT_IMAGE_NAME.withTag(tag)); } @Override public JdbcDatabaseContainer newInstance(ConnectionUrl connectionUrl) { return newInstanceFromConnectionUrl(connectionUrl, USER_PARAM, PASSWORD_PARAM); } } ================================================ FILE: modules/yugabytedb/src/main/java/org/testcontainers/containers/delegate/AbstractYCQLDelegate.java ================================================ package org.testcontainers.containers.delegate; import org.testcontainers.delegate.DatabaseDelegate; /** * An abstract delegate do-nothing class */ public abstract class AbstractYCQLDelegate implements DatabaseDelegate { @Override public void execute( String statement, String scriptPath, int lineNumber, boolean continueOnError, boolean ignoreFailedDrops ) { // do nothing } @Override public void close() { // do nothing } } ================================================ FILE: modules/yugabytedb/src/main/java/org/testcontainers/containers/delegate/YugabyteDBYCQLDelegate.java ================================================ package org.testcontainers.containers.delegate; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.testcontainers.containers.Container.ExecResult; import org.testcontainers.containers.YugabyteDBYCQLContainer; import org.testcontainers.ext.ScriptUtils.UncategorizedScriptException; import java.util.Collection; /** * Query execution delegate class for YCQL API to delegate init-script statements. This * invokes the in-built ycqlsh cli within the container to execute the * statements at one go. It is recommended to use frameworks such as liquibase to manage * this requirement. This functionality is kept to address the initialization requirements * from standalone services that can't leverage liquibase or something similar. * * @see YugabyteDBYCQLContainer */ @RequiredArgsConstructor @Slf4j public final class YugabyteDBYCQLDelegate extends AbstractYCQLDelegate { private static final String BIN_PATH = "/home/yugabyte/tserver/bin/ycqlsh"; private final YugabyteDBYCQLContainer container; @Override public void execute( Collection statements, String scriptPath, boolean continueOnError, boolean ignoreFailedDrops ) { final String containerInterfaceIP = container .getContainerInfo() .getNetworkSettings() .getNetworks() .entrySet() .stream() .findFirst() .get() .getValue() .getIpAddress(); try { ExecResult result = container.execInContainer( BIN_PATH, containerInterfaceIP, "-u", container.getUsername(), "-p", container.getPassword(), "-k", container.getKeyspace(), "-e", StringUtils.join(statements, ";") ); if (result.getExitCode() != 0) { throw new RuntimeException(result.getStderr()); } } catch (Exception e) { log.debug(e.getMessage(), e); throw new UncategorizedScriptException(e.getMessage(), e); } } } ================================================ FILE: modules/yugabytedb/src/main/java/org/testcontainers/containers/strategy/YugabyteDBYCQLWaitStrategy.java ================================================ package org.testcontainers.containers.strategy; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.testcontainers.containers.Container.ExecResult; import org.testcontainers.containers.YugabyteDBYCQLContainer; import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy; import org.testcontainers.containers.wait.strategy.WaitStrategyTarget; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import static org.rnorth.ducttape.unreliables.Unreliables.retryUntilSuccess; /** * Custom wait strategy for YCQL API. * *

* Though we can either use HTTP or PORT based wait strategy, when we create a custom * keyspace/role, it gets executed asynchronously. As the wait on container.start() on a * specific port wouldn't fully guarantee the custom object execution. It's better to * check the DB status with this way with a smoke test query that uses the underlying * custom objects and wait for the operation to complete. *

*/ @RequiredArgsConstructor @Slf4j public final class YugabyteDBYCQLWaitStrategy extends AbstractWaitStrategy { private static final String YCQL_TEST_QUERY = "SELECT release_version FROM system.local"; private static final String BIN_PATH = "/home/yugabyte/tserver/bin/ycqlsh"; private final WaitStrategyTarget target; @Override public void waitUntilReady(WaitStrategyTarget target) { YugabyteDBYCQLContainer container = (YugabyteDBYCQLContainer) target; AtomicBoolean status = new AtomicBoolean(true); final String containerInterfaceIP = container .getContainerInfo() .getNetworkSettings() .getNetworks() .entrySet() .stream() .findFirst() .get() .getValue() .getIpAddress(); retryUntilSuccess( (int) startupTimeout.getSeconds(), TimeUnit.SECONDS, () -> { YugabyteDBYCQLWaitStrategy.this.getRateLimiter() .doWhenReady(() -> { try { ExecResult result = container.execInContainer( BIN_PATH, containerInterfaceIP, "-u", container.getUsername(), "-p", container.getPassword(), "-k", container.getKeyspace(), "-e", YCQL_TEST_QUERY ); if (result.getExitCode() != 0) { status.set(false); log.debug(result.getStderr()); } } catch (Exception e) { status.set(false); log.debug(e.getMessage(), e); } finally { if (!status.getAndSet(true)) { throw new RuntimeException("container hasn't come up yet"); } } }); return status; } ); } @Override public void waitUntilReady() { waitUntilReady(target); } } ================================================ FILE: modules/yugabytedb/src/main/java/org/testcontainers/containers/strategy/YugabyteDBYSQLWaitStrategy.java ================================================ package org.testcontainers.containers.strategy; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.testcontainers.containers.YugabyteDBYSQLContainer; import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy; import org.testcontainers.containers.wait.strategy.WaitStrategyTarget; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; import java.util.concurrent.TimeUnit; import static org.rnorth.ducttape.unreliables.Unreliables.retryUntilSuccess; /** * Custom wait strategy for YSQL API. * *

* Though we can either use HTTP or PORT based wait strategy, when we create a custom * database/role, it gets executed asynchronously. As the wait on container.start() on a * specific port wouldn't fully guarantee the custom object execution. It's better to * check the DB status with this way with a smoke test query that uses the underlying * custom objects and wait for the operation to complete. *

*/ @RequiredArgsConstructor @Slf4j public final class YugabyteDBYSQLWaitStrategy extends AbstractWaitStrategy { private final WaitStrategyTarget target; private static final String YSQL_EXTENDED_PROBE = "CREATE TABLE IF NOT EXISTS YB_SAMPLE(k int, v int, primary key(k, v))"; private static final String YSQL_EXTENDED_PROBE_DROP_TABLE = "DROP TABLE IF EXISTS YB_SAMPLE"; @Override public void waitUntilReady(WaitStrategyTarget target) { YugabyteDBYSQLContainer container = (YugabyteDBYSQLContainer) target; retryUntilSuccess( (int) startupTimeout.getSeconds(), TimeUnit.SECONDS, () -> { getRateLimiter() .doWhenReady(() -> { try (Connection con = container.createConnection(""); Statement stmt = con.createStatement()) { stmt.execute(YSQL_EXTENDED_PROBE); stmt.execute(YSQL_EXTENDED_PROBE_DROP_TABLE); } catch (SQLException ex) { throw new RuntimeException(ex); } }); return true; } ); } @Override public void waitUntilReady() { waitUntilReady(target); } } ================================================ FILE: modules/yugabytedb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider ================================================ org.testcontainers.containers.YugabyteDBYSQLContainerProvider ================================================ FILE: modules/yugabytedb/src/test/java/org/testcontainers/jdbc/yugabytedb/YugabyteDBYSQLJDBCDriverTest.java ================================================ package org.testcontainers.jdbc.yugabytedb; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; /** * YugabyteDB YSQL API JDBC connectivity driver test class */ class YugabyteDBYSQLJDBCDriverTest extends AbstractJDBCDriverTest { public static Iterable data() { return Arrays.asList( new Object[][] { { "jdbc:tc:yugabyte://hostname/yugabyte?user=yugabyte&password=yugabyte", EnumSet.noneOf(Options.class), }, } ); } } ================================================ FILE: modules/yugabytedb/src/test/java/org/testcontainers/junit/yugabytedb/YugabyteDBYCQLTest.java ================================================ package org.testcontainers.junit.yugabytedb; import com.datastax.oss.driver.api.core.CqlSession; import com.datastax.oss.driver.api.core.cql.ResultSet; import org.junit.jupiter.api.Test; import org.testcontainers.containers.YugabyteDBYCQLContainer; import org.testcontainers.utility.DockerImageName; import static org.assertj.core.api.Assertions.assertThat; /** * YugabyteDB YCQL API unit test class */ class YugabyteDBYCQLTest { private static final String IMAGE_NAME = "yugabytedb/yugabyte:2.14.4.0-b26"; private static final String IMAGE_NAME_2_18 = "yugabytedb/yugabyte:2.18.3.0-b75"; private static final DockerImageName YBDB_TEST_IMAGE = DockerImageName.parse(IMAGE_NAME); @Test void testSmoke() { try ( // creatingYCQLContainer { final YugabyteDBYCQLContainer ycqlContainer = new YugabyteDBYCQLContainer( "yugabytedb/yugabyte:2.14.4.0-b26" ) .withUsername("cassandra") .withPassword("cassandra") // } ) { ycqlContainer.start(); assertThat(performQuery(ycqlContainer, "SELECT release_version FROM system.local").wasApplied()) .as("A sample test query succeeds") .isTrue(); } } @Test void testCustomKeyspace() { String key = "random"; try ( final YugabyteDBYCQLContainer ycqlContainer = new YugabyteDBYCQLContainer(YBDB_TEST_IMAGE) .withKeyspaceName(key) .withUsername("cassandra") .withPassword("cassandra") ) { ycqlContainer.start(); assertThat( performQuery( ycqlContainer, "SELECT keyspace_name FROM system_schema.keyspaces where keyspace_name='" + key + "'" ) .one() .getString(0) ) .as("Custom keyspace creation succeeds") .isEqualTo(key); } } @Test void testAuthenticationEnabled() { String role = "random"; try ( final YugabyteDBYCQLContainer ycqlContainer = new YugabyteDBYCQLContainer(YBDB_TEST_IMAGE) .withUsername(role) .withPassword(role) ) { ycqlContainer.start(); assertThat( performQuery(ycqlContainer, "SELECT role FROM system_auth.roles where role='" + role + "'") .one() .getString(0) ) .as("Keyspace login with authentication enabled succeeds") .isEqualTo(role); } } @Test void testAuthenticationDisabled() { try ( final YugabyteDBYCQLContainer ycqlContainer = new YugabyteDBYCQLContainer(YBDB_TEST_IMAGE) .withPassword("cassandra") .withUsername("cassandra") ) { ycqlContainer.start(); assertThat(performQuery(ycqlContainer, "SELECT release_version FROM system.local").wasApplied()) .as("Keyspace login with authentication disabled succeeds") .isTrue(); } } @Test void testInitScript() { String key = "random"; try ( final YugabyteDBYCQLContainer ycqlContainer = new YugabyteDBYCQLContainer(YBDB_TEST_IMAGE) .withKeyspaceName(key) .withUsername(key) .withPassword(key) .withInitScript("init/init_yql.sql") ) { ycqlContainer.start(); ResultSet output = performQuery(ycqlContainer, "SELECT greet FROM random.dsql"); assertThat(output.wasApplied()).as("Statements from a custom script execution succeeds").isTrue(); assertThat(output.one().getString(0)).as("A record match succeeds").isEqualTo("Hello DSQL"); } } @Test void shouldStartWhenContainerIpIsUsedInWaitStrategy() { try ( final YugabyteDBYCQLContainer ycqlContainer = new YugabyteDBYCQLContainer(IMAGE_NAME_2_18) .withUsername("cassandra") .withPassword("cassandra") ) { ycqlContainer.start(); boolean isQueryExecuted = performQuery(ycqlContainer, "SELECT release_version FROM system.local") .wasApplied(); assertThat(isQueryExecuted).isTrue(); } } private ResultSet performQuery(YugabyteDBYCQLContainer ycqlContainer, String cql) { try ( CqlSession session = CqlSession .builder() .withKeyspace(ycqlContainer.getKeyspace()) .withAuthCredentials(ycqlContainer.getUsername(), ycqlContainer.getPassword()) .withLocalDatacenter(ycqlContainer.getLocalDc()) .addContactPoint(ycqlContainer.getContactPoint()) .build() ) { return session.execute(cql); } } } ================================================ FILE: modules/yugabytedb/src/test/java/org/testcontainers/junit/yugabytedb/YugabyteDBYSQLTest.java ================================================ package org.testcontainers.junit.yugabytedb; import org.junit.jupiter.api.Test; import org.testcontainers.containers.YugabyteDBYSQLContainer; import org.testcontainers.db.AbstractContainerDatabaseTest; import org.testcontainers.utility.DockerImageName; import java.sql.SQLException; import static org.assertj.core.api.Assertions.assertThat; /** * YugabyteDB YSQL API unit test class */ class YugabyteDBYSQLTest extends AbstractContainerDatabaseTest { private static final String IMAGE_NAME = "yugabytedb/yugabyte:2.14.4.0-b26"; private static final DockerImageName YBDB_TEST_IMAGE = DockerImageName.parse(IMAGE_NAME); @Test void testSmoke() throws SQLException { try ( // creatingYSQLContainer { final YugabyteDBYSQLContainer ysqlContainer = new YugabyteDBYSQLContainer( "yugabytedb/yugabyte:2.14.4.0-b26" ) // } ) { ysqlContainer.start(); assertThat(performQuery(ysqlContainer, "SELECT 1").getInt(1)) .as("A sample test query succeeds") .isEqualTo(1); } } @Test void testCustomDatabase() throws SQLException { String key = "random"; try ( final YugabyteDBYSQLContainer ysqlContainer = new YugabyteDBYSQLContainer(YBDB_TEST_IMAGE) .withDatabaseName(key) ) { ysqlContainer.start(); assertThat(performQuery(ysqlContainer, "SELECT 1").getInt(1)) .as("A test query on a custom database succeeds") .isEqualTo(1); } } @Test void testInitScript() throws SQLException { try ( final YugabyteDBYSQLContainer ysqlContainer = new YugabyteDBYSQLContainer(YBDB_TEST_IMAGE) .withInitScript("init/init_yql.sql") ) { ysqlContainer.start(); assertThat(performQuery(ysqlContainer, "SELECT greet FROM dsql").getString(1)) .as("A record match succeeds") .isEqualTo("Hello DSQL"); } } @Test void testWithAdditionalUrlParamInJdbcUrl() { try ( final YugabyteDBYSQLContainer ysqlContainer = new YugabyteDBYSQLContainer(YBDB_TEST_IMAGE) .withUrlParam("sslmode", "disable") .withUrlParam("application_name", "yugabyte") ) { ysqlContainer.start(); String jdbcUrl = ysqlContainer.getJdbcUrl(); assertThat(jdbcUrl) .contains("?") .contains("&") .contains("sslmode=disable") .contains("application_name=yugabyte") .as("A JDBC connection string with additional parameter validation succeeds"); } } @Test void testWithCustomRole() throws SQLException { try ( final YugabyteDBYSQLContainer ysqlContainer = new YugabyteDBYSQLContainer(YBDB_TEST_IMAGE) .withDatabaseName("yugabyte") .withPassword("yugabyte") .withUsername("yugabyte") ) { ysqlContainer.start(); assertThat(performQuery(ysqlContainer, "SELECT 1").getInt(1)) .as("A sample test query with a custom role succeeds") .isEqualTo(1); } } @Test void testWaitStrategy() throws SQLException { try (final YugabyteDBYSQLContainer ysqlContainer = new YugabyteDBYSQLContainer(YBDB_TEST_IMAGE)) { ysqlContainer.start(); assertThat(performQuery(ysqlContainer, "SELECT 1").getInt(1)) .as("A sample test query succeeds") .isEqualTo(1); boolean tableExists = performQuery( ysqlContainer, "SELECT EXISTS (SELECT FROM pg_tables WHERE tablename = 'YB_SAMPLE')" ) .getBoolean(1); assertThat(tableExists).as("yb_sample table does not exists").isFalse(); } } } ================================================ FILE: modules/yugabytedb/src/test/resources/init/init_yql.sql ================================================ CREATE TABLE dsql( greet text primary key ); INSERT INTO dsql (greet) VALUES ('Hello DSQL'); ================================================ FILE: modules/yugabytedb/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: requirements.txt ================================================ mkdocs==1.3.0 mkdocs-codeinclude-plugin==0.2.0 mkdocs-material==8.1.3 mkdocs-markdownextradata-plugin==0.2.5 ================================================ FILE: runtime.txt ================================================ 3.8 ================================================ FILE: settings.gradle ================================================ buildscript { repositories { maven { url "https://plugins.gradle.org/m2/" } } dependencies { classpath "com.gradle.enterprise:com.gradle.enterprise.gradle.plugin:3.19.2" classpath "com.gradle:common-custom-user-data-gradle-plugin:2.4.0" classpath "org.gradle.toolchains:foojay-resolver:0.8.0" } } apply plugin: 'com.gradle.develocity' apply plugin: "com.gradle.common-custom-user-data-gradle-plugin" apply plugin: "org.gradle.toolchains.foojay-resolver-convention" rootProject.name = 'testcontainers-java' include "bom" include "testcontainers" project(':testcontainers').projectDir = "$rootDir/core" as File file('modules').eachDir { dir -> include "testcontainers-${dir.name}" project(":testcontainers-${dir.name}").projectDir = dir } include ':docs:examples:junit5:redis' include ':docs:examples:spock:redis' include 'test-support' ext.isCI = System.getenv("CI") != null buildCache { local { enabled = !isCI } remote(develocity.buildCache) { push = isCI && !System.getenv("READ_ONLY_REMOTE_GRADLE_CACHE") && System.getenv("DEVELOCITY_ACCESS_KEY") enabled = true } } develocity { buildScan { server = "https://ge.testcontainers.org/" publishing.onlyIf { it.authenticated } uploadInBackground = !isCI capture.fileFingerprints = true } } ================================================ FILE: smoke-test/build.gradle ================================================ // empty build.gradle for dependabot plugins { id 'com.diffplug.spotless' version '6.22.0' apply false } apply from: "$rootDir/../gradle/ci-support.gradle" subprojects { apply plugin: "java" apply from: "$rootDir/../gradle/spotless.gradle" apply plugin: 'checkstyle' repositories { mavenCentral() } test { defaultCharacterEncoding = "UTF-8" testLogging { displayGranularity 1 showStackTraces = true exceptionFormat = 'full' events "STARTED", "PASSED", "FAILED", "SKIPPED" } } checkstyle { toolVersion = "10.23.0" configFile = rootProject.file('../config/checkstyle/checkstyle.xml') } } ================================================ FILE: smoke-test/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531 distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: smoke-test/gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: smoke-test/gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @rem SPDX-License-Identifier: Apache-2.0 @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line set CLASSPATH= @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: smoke-test/settings.gradle ================================================ buildscript { repositories { maven { url "https://plugins.gradle.org/m2/" } } dependencies { classpath "gradle.plugin.ch.myniva.gradle:s3-build-cache:0.10.0" classpath "com.gradle.enterprise:com.gradle.enterprise.gradle.plugin:3.17.4" classpath "com.gradle:common-custom-user-data-gradle-plugin:2.0.1" } } apply plugin: 'com.gradle.develocity' apply plugin: "com.gradle.common-custom-user-data-gradle-plugin" rootProject.name = 'testcontainers-smoke-tests' includeBuild '..' // explicit include to allow Dependabot to autodiscover subprojects include 'turbo-mode' ext.isCI = System.getenv("CI") != null buildCache { local { enabled = !isCI } remote(develocity.buildCache) { push = isCI && !System.getenv("READ_ONLY_REMOTE_GRADLE_CACHE") && System.getenv("DEVELOCITY_ACCESS_KEY") enabled = true } } develocity { buildScan { server = "https://ge.testcontainers.org/" publishing.onlyIf { it.authenticated } uploadInBackground = !isCI capture.fileFingerprints = true } } ================================================ FILE: smoke-test/turbo-mode/build.gradle ================================================ plugins { id 'java' } dependencies { testImplementation 'org.testcontainers:testcontainers' testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' testImplementation 'ch.qos.logback:logback-classic:1.3.15' testImplementation 'org.assertj:assertj-core:3.27.4' testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.13.3' } test { useJUnitPlatform() forkEvery = 1 maxParallelForks = 4 } ================================================ FILE: smoke-test/turbo-mode/src/test/java/org/testcontainers/example/AbstractRedisContainer.java ================================================ package org.testcontainers.example; import org.testcontainers.containers.GenericContainer; import static org.assertj.core.api.Assertions.assertThat; class AbstractRedisContainer { private static final String REDIS_IMAGE = "redis:7.0.12-alpine"; void runRedisContainer() { try ( GenericContainer redis = new GenericContainer<>(REDIS_IMAGE) .withExposedPorts(6379) .withCreateContainerCmdModifier(cmd -> cmd.withName("tc-redis")) ) { redis.start(); assertThat(redis.isRunning()).isTrue(); } } } ================================================ FILE: smoke-test/turbo-mode/src/test/java/org/testcontainers/example/RedisContainer1Test.java ================================================ package org.testcontainers.example; import org.junit.jupiter.api.Test; class RedisContainer1Test extends AbstractRedisContainer { @Test void testSimple() { runRedisContainer(); } } ================================================ FILE: smoke-test/turbo-mode/src/test/java/org/testcontainers/example/RedisContainer2Test.java ================================================ package org.testcontainers.example; import org.junit.jupiter.api.Test; class RedisContainer2Test extends AbstractRedisContainer { @Test void testSimple() { runRedisContainer(); } } ================================================ FILE: smoke-test/turbo-mode/src/test/java/org/testcontainers/example/RedisContainer3Test.java ================================================ package org.testcontainers.example; import org.junit.jupiter.api.Test; class RedisContainer3Test extends AbstractRedisContainer { @Test void testSimple() { runRedisContainer(); } } ================================================ FILE: smoke-test/turbo-mode/src/test/java/org/testcontainers/example/RedisContainer4Test.java ================================================ package org.testcontainers.example; import org.junit.jupiter.api.Test; class RedisContainer4Test extends AbstractRedisContainer { @Test void testSimple() { runRedisContainer(); } } ================================================ FILE: smoke-test/turbo-mode/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n ================================================ FILE: test-support/build.gradle ================================================ dependencies { implementation 'junit:junit:4.13.2' implementation 'org.slf4j:slf4j-api:2.0.17' testImplementation 'org.assertj:assertj-core:3.27.4' } ================================================ FILE: test-support/src/main/java/org/testcontainers/testsupport/Flaky.java ================================================ package org.testcontainers.testsupport; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Annotation for test methods that should be retried in the event of failure. See {@link FlakyTestJUnit4RetryRule} for * more details. */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD }) public @interface Flaky { /** * @return a URL for a GitHub issue where this flaky test can be discussed, and where actions to resolve it can be * coordinated. */ String githubIssueUrl(); /** * @return a date at which this should be reviewed, in {@link java.time.format.DateTimeFormatter#ISO_LOCAL_DATE} * format (e.g. {@code 2020-12-03}). Now + 3 months is suggested. Once this date has passed, retries will no longer * be applied. */ String reviewDate(); /** * @return the total number of times to try running this test (default 3) */ int maxTries() default 3; } ================================================ FILE: test-support/src/main/java/org/testcontainers/testsupport/FlakyTestJUnit4RetryRule.java ================================================ package org.testcontainers.testsupport; import lombok.extern.slf4j.Slf4j; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.MultipleFailureException; import org.junit.runners.model.Statement; import java.time.LocalDate; import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.List; /** *

* JUnit 4 @Rule that implements retry for flaky tests (tests that suffer from sporadic random failures). *

*

* This rule should be used in conjunction with the @{@link Flaky} annotation. When this Rule is applied to a test * class, any test method with this annotation will be invoked up to 3 times or until it succeeds. *

*

* Tests should not be marked @{@link Flaky} for a long period of time. Every usage should be * accompanied by a GitHub issue URL, and should be subject to review at a suitable point in the (near) future. * Should the review date pass without the test's instability being fixed, the retry behaviour will cease to have an * effect and the test will be allowed to sporadically fail again. *

*/ @Slf4j public class FlakyTestJUnit4RetryRule implements TestRule { @Override public Statement apply(Statement base, Description description) { final Flaky annotation = description.getAnnotation(Flaky.class); if (annotation == null) { // leave the statement as-is return base; } if (annotation.githubIssueUrl().trim().length() == 0) { throw new IllegalArgumentException("A GitHub issue URL must be set for usages of the @Flaky annotation"); } final int maxTries = annotation.maxTries(); if (maxTries < 1) { throw new IllegalArgumentException("@Flaky annotation maxTries must be at least one"); } final LocalDate reviewDate; try { reviewDate = LocalDate.parse(annotation.reviewDate()); } catch (DateTimeParseException e) { throw new IllegalArgumentException( "@Flaky reviewDate could not be parsed. Please provide a date in yyyy-mm-dd format" ); } // the annotation should only have an effect before the review date, to encourage review and resolution if (LocalDate.now().isBefore(reviewDate)) { return new RetryingStatement(base, description, maxTries); } else { return base; } } private static class RetryingStatement extends Statement { private final Statement base; private final Description description; private final int maxTries; RetryingStatement(Statement base, Description description, int maxTries) { this.base = base; this.description = description; this.maxTries = maxTries; } @Override public void evaluate() { int attempts = 0; final List causes = new ArrayList<>(); while (++attempts <= maxTries) { try { base.evaluate(); return; } catch (Throwable throwable) { log.warn("Retrying @Flaky-annotated test: {}", description.getDisplayName()); causes.add(throwable); } } throw new IllegalStateException( "@Flaky-annotated test failed despite retries.", new MultipleFailureException(causes) ); } } } ================================================ FILE: test-support/src/test/java/org/testcontainers/testsupport/FlakyRuleTest.java ================================================ package org.testcontainers.testsupport; import org.junit.Test; import org.junit.runner.Description; import org.junit.runners.model.Statement; import java.lang.annotation.Annotation; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; public class FlakyRuleTest { private static final String VALID_URL = "http://some.url/here"; private static final String INVALID_URL = ""; private static final String VALID_DATE_IN_FAR_FUTURE = "2063-04-05"; private static final String VALID_DATE_IN_PAST = "1991-08-16"; private static final String INVALID_DATE = "91-01-45"; private static final int DEFAULT_TRIES = 3; private FlakyTestJUnit4RetryRule rule = new FlakyTestJUnit4RetryRule(); @Test public void testIgnoresMethodWithoutAnnotation() throws Throwable { final Description description = newDescriptionWithoutAnnotation(); final DummyStatement statement = newStatement(3); try { rule.apply(statement, description).evaluate(); fail("Should not reach here"); } catch (Exception ignored) {} assertThat(statement.invocationCount) .as("The statement should only be invoked once, even if it throws") .isEqualTo(1); } @Test public void testRetriesMethodWithAnnotationUntilFailure() throws Throwable { final Description description = newDescriptionWithAnnotation(); final DummyStatement statement = newStatement(3); try { rule.apply(statement, description).evaluate(); fail("Should not reach here"); } catch (Exception ignored) {} assertThat(statement.invocationCount).as("The statement should be invoked three times").isEqualTo(3); } @Test public void testCustomRetryCount() throws Throwable { final Description description = newDescriptionWithAnnotationAndCustomTries(10); final DummyStatement statement = newStatement(10); try { rule.apply(statement, description).evaluate(); fail("Should not reach here"); } catch (Exception ignored) {} assertThat(statement.invocationCount).as("The statement should be invoked ten times").isEqualTo(10); } @Test public void testRetriesMethodWithAnnotationUntilSuccess() throws Throwable { final Description description = newDescriptionWithAnnotation(); final DummyStatement statement = newStatement(2); rule.apply(statement, description).evaluate(); assertThat(statement.invocationCount) .as("The statement should be invoked three times, and succeed the third time") .isEqualTo(3); } @Test public void testDoesNotRetryMethodWithAnnotationIfNotThrowing() throws Throwable { final Description description = newDescriptionWithAnnotation(); final DummyStatement statement = newStatement(0); rule.apply(statement, description).evaluate(); assertThat(statement.invocationCount).as("The statement should be invoked once").isEqualTo(1); } @Test public void testTreatsExpiredAnnotationAsNoAnnotation() throws Throwable { final Description description = newDescriptionWithExpiredAnnotation(); final DummyStatement statement = newStatement(3); try { rule.apply(statement, description).evaluate(); fail("Should not reach here"); } catch (Exception ignored) {} assertThat(statement.invocationCount) .as("The statement should only be invoked once, even if it throws") .isEqualTo(1); } @Test public void testThrowsOnInvalidDateFormat() throws Throwable { final Description description = newDescriptionWithAnnotation(INVALID_DATE, VALID_URL); final DummyStatement statement = newStatement(3); try { rule.apply(statement, description).evaluate(); fail("Should not reach here"); } catch (IllegalArgumentException ignored) {} assertThat(statement.invocationCount) .as("The statement should not be invoked if the annotation is invalid") .isEqualTo(0); } @Test public void testThrowsOnInvalidGitHubUrl() throws Throwable { final Description description = newDescriptionWithAnnotation(VALID_DATE_IN_FAR_FUTURE, INVALID_URL); final DummyStatement statement = newStatement(3); try { rule.apply(statement, description).evaluate(); fail("Should not reach here"); } catch (IllegalArgumentException ignored) {} assertThat(statement.invocationCount) .as("The statement should not be invoked if the annotation is invalid") .isEqualTo(0); } private Description newDescriptionWithAnnotation(String reviewDate, String gitHubUrl) { return Description.createTestDescription( "SomeTestClass", "someMethod", newAnnotation(reviewDate, gitHubUrl, DEFAULT_TRIES) ); } private Description newDescriptionWithoutAnnotation() { return Description.createTestDescription("SomeTestClass", "someMethod"); } private Description newDescriptionWithAnnotation() { return Description.createTestDescription( "SomeTestClass", "someMethod", newAnnotation(VALID_DATE_IN_FAR_FUTURE, VALID_URL, DEFAULT_TRIES) ); } private Description newDescriptionWithAnnotationAndCustomTries(int maxTries) { return Description.createTestDescription( "SomeTestClass", "someMethod", newAnnotation(VALID_DATE_IN_FAR_FUTURE, VALID_URL, maxTries) ); } private Description newDescriptionWithExpiredAnnotation() { return Description.createTestDescription( "SomeTestClass", "someMethod", newAnnotation(VALID_DATE_IN_PAST, VALID_URL, DEFAULT_TRIES) ); } private Flaky newAnnotation(final String reviewDate, String gitHubUrl, int tries) { return new Flaky() { @Override public Class annotationType() { return Flaky.class; } @Override public String githubIssueUrl() { return gitHubUrl; } @Override public String reviewDate() { return reviewDate; } @Override public int maxTries() { return tries; } }; } private DummyStatement newStatement(int timesToThrow) { final DummyStatement statement = new DummyStatement(); statement.shouldThrowTimes = timesToThrow; return statement; } private static class DummyStatement extends Statement { int shouldThrowTimes = -1; int invocationCount = 0; @Override public void evaluate() { invocationCount++; if (shouldThrowTimes > 0) { shouldThrowTimes--; throw new RuntimeException(); } } } } ================================================ FILE: test-support/src/test/resources/logback-test.xml ================================================ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n