diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3a7ea347cb3..9131ce06da3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,23 +1,13 @@ // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: // https://github.com/microsoft/vscode-dev-containers/tree/v0.241.1/containers/java-8 { - "name": "Java 8", - "image": "mcr.microsoft.com/devcontainers/java:1-8-bullseye", + "name": "Java 17", + "image": "mcr.microsoft.com/devcontainers/java:0-17", // Configure tool-specific properties. "customizations": { // Configure properties specific to VS Code. "vscode": { - // Set *default* container specific settings.json values on container create. - "settings": { - "java.import.gradle.java.home": "/usr/local/sdkman/candidates/java/current", - "java.configuration.runtimes": [{ - "default": true, - "name": "JavaSE-1.8", - "path": "/usr/local/sdkman/candidates/java/current" - }] - }, - // Add the IDs of extensions you want installed when the container is created. "extensions": [ "vscjava.vscode-java-pack" diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 81e3dbbd900..c31dd05e048 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -17,21 +17,27 @@ body: - 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 @@ -39,22 +45,31 @@ body: - 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 diff --git a/.github/ISSUE_TEMPLATE/enhancement.yaml b/.github/ISSUE_TEMPLATE/enhancement.yaml index d073f8f1674..9b9a06ecf6a 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.yaml +++ b/.github/ISSUE_TEMPLATE/enhancement.yaml @@ -17,21 +17,27 @@ body: - 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 @@ -39,22 +45,31 @@ body: - 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 diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml index dedccecd4e5..b655b4ac505 100644 --- a/.github/ISSUE_TEMPLATE/feature.yaml +++ b/.github/ISSUE_TEMPLATE/feature.yaml @@ -17,21 +17,27 @@ body: - 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 @@ -39,22 +45,31 @@ body: - 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 @@ -75,7 +90,7 @@ body: id: benefit attributes: label: Benefit - description: What's the benefit of addng this feature to the project? + description: What's the benefit of adding this feature to the project? validations: required: true - type: textarea diff --git a/.github/actions/setup-build/action.yml b/.github/actions/setup-build/action.yml index 23204529408..0ab491cbb20 100644 --- a/.github/actions/setup-build/action.yml +++ b/.github/actions/setup-build/action.yml @@ -1,9 +1,16 @@ name: Set up Build description: Sets up Build +inputs: + java-version: + description: 'The Java version to set up' + required: true + default: '17' runs: using: "composite" steps: - uses: ./.github/actions/setup-java + with: + java-version: ${{ inputs.java-version }} - name: Clear existing docker image cache shell: bash run: docker image prune -af diff --git a/.github/actions/setup-gradle/action.yml b/.github/actions/setup-gradle/action.yml index 58499f75e57..76d1b0ff4b2 100644 --- a/.github/actions/setup-gradle/action.yml +++ b/.github/actions/setup-gradle/action.yml @@ -4,10 +4,9 @@ runs: using: "composite" steps: - name: Setup Gradle Build Action - uses: gradle/gradle-build-action@v2 + uses: gradle/actions/setup-gradle@v6 with: gradle-home-cache-includes: | caches notifications jdks - gradle-home-cache-cleanup: true diff --git a/.github/actions/setup-java/action.yml b/.github/actions/setup-java/action.yml index 8d9733f02ba..093fbc2e7fd 100644 --- a/.github/actions/setup-java/action.yml +++ b/.github/actions/setup-java/action.yml @@ -1,9 +1,14 @@ name: Set up Java description: Sets up Java version +inputs: + java-version: + description: 'The Java version to set up' + required: true + default: '17' runs: using: "composite" steps: - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v5 with: - java-version: '8' + java-version: ${{ inputs.java-version }} distribution: temurin diff --git a/.github/actions/setup-junit-report/action.yml b/.github/actions/setup-junit-report/action.yml index 86092d06bcc..df0af39297f 100644 --- a/.github/actions/setup-junit-report/action.yml +++ b/.github/actions/setup-junit-report/action.yml @@ -4,7 +4,7 @@ runs: using: "composite" steps: - name: Publish Test Report - uses: mikepenz/action-junit-report@v3 + uses: mikepenz/action-junit-report@v6 if: always() # always run even if the previous step fails with: report_paths: '**/build/test-results/test/TEST-*.xml' diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e3506109406..789ec351da2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -16,10 +16,14 @@ updates: update-types: [ "version-update:semver-major" ] - dependency-name: "org.mockito:mockito-core" update-types: [ "version-update:semver-major" ] - - dependency-name: "org.yaml:snakeyaml" - 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" ] + - dependency-name: "io.rest-assured:rest-assured" + update-types: [ "version-update:semver-major" ] - package-ecosystem: "gradle" directory: "/" allow: @@ -29,6 +33,13 @@ updates: 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" @@ -39,80 +50,106 @@ updates: - package-ecosystem: "gradle" directory: "/modules/azure" schedule: - interval: "weekly" + interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/cassandra" schedule: - interval: "weekly" + 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 + ignore: + - dependency-name: "io.rest-assured:rest-assured" + update-types: [ "version-update:semver-major" ] - package-ecosystem: "gradle" directory: "/modules/clickhouse" schedule: - interval: "weekly" + interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/cockroachdb" schedule: - interval: "weekly" + interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/consul" schedule: - interval: "weekly" + interval: "monthly" open-pull-requests-limit: 10 + ignore: + - dependency-name: "io.rest-assured:rest-assured" + update-types: [ "version-update:semver-major" ] - package-ecosystem: "gradle" directory: "/modules/couchbase" schedule: - interval: "weekly" + interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/cratedb" schedule: - interval: "weekly" + interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/database-commons" schedule: - interval: "weekly" + interval: "monthly" + - package-ecosystem: "gradle" + directory: "/modules/databend" + schedule: + interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/db2" schedule: - interval: "weekly" + interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/dynalite" schedule: - interval: "weekly" + interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/elasticsearch" schedule: - interval: "weekly" + interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/gcloud" schedule: - interval: "weekly" + interval: "monthly" open-pull-requests-limit: 10 + - package-ecosystem: "gradle" + directory: "/modules/grafana" + schedule: + interval: "monthly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "io.rest-assured:rest-assured" + update-types: [ "version-update:semver-major" ] - package-ecosystem: "gradle" directory: "/modules/hivemq" schedule: - interval: "weekly" + interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/influxdb" schedule: - interval: "weekly" + 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: "weekly" + interval: "monthly" open-pull-requests-limit: 10 ignore: - dependency-name: "org.mockito:mockito-core" @@ -120,74 +157,96 @@ updates: - package-ecosystem: "gradle" directory: "/modules/jdbc-test" schedule: - interval: "weekly" + 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: "weekly" + 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: "weekly" + 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: "weekly" + 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: "weekly" + interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/mariadb" schedule: - interval: "weekly" + 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: "weekly" + interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/mockserver" schedule: - interval: "weekly" + interval: "monthly" open-pull-requests-limit: 10 + ignore: + - dependency-name: "io.rest-assured:rest-assured" + update-types: [ "version-update:semver-major" ] - package-ecosystem: "gradle" directory: "/modules/mongodb" schedule: - interval: "weekly" + interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/mssqlserver" schedule: - interval: "weekly" + interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/mysql" schedule: - interval: "weekly" + interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/neo4j" schedule: - interval: "weekly" + interval: "monthly" open-pull-requests-limit: 10 ignore: - dependency-name: "org.neo4j.driver:neo4j-java-driver" @@ -197,64 +256,107 @@ updates: - package-ecosystem: "gradle" directory: "/modules/nginx" schedule: - interval: "weekly" + 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 + ignore: + - dependency-name: "io.rest-assured:rest-assured" + update-types: [ "version-update:semver-major" ] + - package-ecosystem: "gradle" + directory: "/modules/openfga" + schedule: + interval: "monthly" - package-ecosystem: "gradle" directory: "/modules/oracle-free" schedule: - interval: "weekly" + interval: "monthly" - package-ecosystem: "gradle" directory: "/modules/oracle-xe" schedule: - interval: "weekly" + interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/orientdb" schedule: - interval: "weekly" + interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/postgresql" schedule: - interval: "weekly" + interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/presto" schedule: - interval: "weekly" + 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: "weekly" + 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: "weekly" + interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/r2dbc" schedule: - interval: "weekly" + 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: "weekly" + interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/redpanda" schedule: - interval: "weekly" + interval: "monthly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "io.rest-assured:rest-assured" + update-types: [ "version-update:semver-major" ] + - package-ecosystem: "gradle" + directory: "/modules/scylladb" + schedule: + interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/selenium" schedule: - interval: "weekly" + interval: "monthly" open-pull-requests-limit: 10 ignore: - dependency-name: "org.seleniumhq.selenium:selenium-bom" @@ -262,7 +364,7 @@ updates: - package-ecosystem: "gradle" directory: "/modules/solace" schedule: - interval: "weekly" + interval: "monthly" open-pull-requests-limit: 10 ignore: - dependency-name: "org.apache.qpid:qpid-jms-client" @@ -270,7 +372,7 @@ updates: - package-ecosystem: "gradle" directory: "/modules/solr" schedule: - interval: "weekly" + interval: "monthly" open-pull-requests-limit: 10 ignore: - dependency-name: "org.apache.solr:solr-solrj" @@ -278,39 +380,60 @@ updates: - package-ecosystem: "gradle" directory: "/modules/spock" schedule: - interval: "weekly" + 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: "weekly" + 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: "weekly" + interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/trino" schedule: - interval: "weekly" + 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: "weekly" + interval: "monthly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "io.rest-assured:rest-assured" + update-types: [ "version-update:semver-major" ] + - package-ecosystem: "gradle" + directory: "/modules/weaviate" + schedule: + interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/yugabytedb" schedule: - interval: "weekly" + interval: "monthly" open-pull-requests-limit: 10 # Examples - package-ecosystem: "gradle" directory: "/examples" schedule: - interval: "weekly" + interval: "monthly" open-pull-requests-limit: 10 ignore: - dependency-name: "ch.qos.logback:logback-classic" @@ -326,11 +449,41 @@ updates: - 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: "org.junit:junit-bom" + 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: "/" + directories: + - /.github/workflows + - /.github/actions/** schedule: interval: "weekly" open-pull-requests-limit: 10 diff --git a/.github/labeler.yml b/.github/labeler.yml index e3bce388744..f4649bd7f99 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -5,6 +5,10 @@ - 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: @@ -27,6 +31,10 @@ - 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: @@ -47,6 +55,10 @@ - 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: @@ -63,6 +75,10 @@ - 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: @@ -83,10 +99,18 @@ - 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: @@ -95,6 +119,10 @@ - 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: @@ -123,6 +151,18 @@ - 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: @@ -132,6 +172,10 @@ - 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: @@ -144,6 +188,10 @@ - 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: @@ -160,6 +208,10 @@ - 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: @@ -180,6 +232,10 @@ - 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: @@ -188,10 +244,18 @@ - 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: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 31553ebfff1..6137bf623e3 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,6 +1,8 @@ -[Creating a Redis container (JUnit 4)](../examples/junit4/generic/src/test/java/generic/ContainerCreationTest.java) inside_block:simple +[Creating a Redis container](../examples/junit5/redis/src/test/java/quickstart/RedisBackedCacheIntTest.java) inside_block:container -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 +The container, as `@Container`, will be started before any tests in the class run, and will be destroyed after all tests have run. diff --git a/docs/features/creating_images.md b/docs/features/creating_images.md index e4e6315a405..8a239d342cc 100644 --- a/docs/features/creating_images.md +++ b/docs/features/creating_images.md @@ -13,9 +13,9 @@ Testcontainers will `docker build` a temporary container image, and will use it `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( +GenericContainer container = new GenericContainer( new ImageFromDockerfile() .withFileFromString("folder/someFile.txt", "hello") .withFileFromClasspath("test.txt", "mappable-resource/test-resource.txt") @@ -51,7 +51,7 @@ new GenericContainer( new ImageFromDockerfile() .withDockerfileFromBuilder(builder -> builder - .from("alpine:3.16") + .from("alpine:3.17") .run("apk add --update nginx") .cmd("nginx", "-g", "daemon off;") .build())) diff --git a/docs/features/image_name_substitution.md b/docs/features/image_name_substitution.md index cf2b09d3d8e..0948e9fa2c7 100644 --- a/docs/features/image_name_substitution.md +++ b/docs/features/image_name_substitution.md @@ -125,6 +125,13 @@ Note that it is also possible to provide this same configuration property: 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 diff --git a/docs/features/reuse.md b/docs/features/reuse.md index b36669876c2..884f3d5ac33 100644 --- a/docs/features/reuse.md +++ b/docs/features/reuse.md @@ -16,7 +16,10 @@ opt-in mechanism per environment. To reuse a container, the container configurat ## How to use it -* Enable `Reusable Containers` in `~/.testcontainers.properties`, by adding `testcontainers.reuse.enable=true` +* 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 diff --git a/docs/index.md b/docs/index.md index 90cb81bf3af..40304dd22ed 100644 --- a/docs/index.md +++ b/docs/index.md @@ -82,7 +82,7 @@ and then use dependencies without specifying a version: ```xml org.testcontainers - mysql + testcontainers-mysql test ``` @@ -92,7 +92,7 @@ Using Gradle 5.0 or higher, you can add the following to the `dependencies` sect === "Gradle" ```groovy implementation platform('org.testcontainers:testcontainers-bom:{{latest_version}}') //import bom - testImplementation('org.testcontainers:mysql') //no version specified + testImplementation('org.testcontainers:testcontainers-mysql') //no version specified ``` @@ -221,6 +221,8 @@ A huge thank you to our sponsors: * [Spark ClickHouse Connector](https://github.com/housepower/spark-clickhouse-connector) - Integration tests for Apache Spark with both single node ClickHouse instance and multi-node ClickHouse cluster. * [Quarkus](https://github.com/quarkusio/quarkus) - Testcontainers is used extensively for Quarkus' [DevServices](https://quarkus.io/guides/dev-services) feature. * [Apache Kyuubi](https://kyuubi.apache.org) - Integration testing with Trino as data source engine, Kafka, etc. +* [Dash0](https://www.dash0.com) - Integration testing for OpenTelemetry Observability product. + ## License diff --git a/docs/modules/activemq.md b/docs/modules/activemq.md index 6f1ef2e9252..7959c47576a 100644 --- a/docs/modules/activemq.md +++ b/docs/modules/activemq.md @@ -1,7 +1,7 @@ # ActiveMQ -Testcontainers module for [ActiveMQ](https://hub.docker.com/r/apache/activemq-classic) and -[Artemis](https://hub.docker.com/r/apache/activemq-artemis). +Testcontainers module for [ActiveMQ](https://hub.docker.com/r/apache/activemq) and +[Artemis](https://hub.docker.com/r/apache/artemis). ## ActiveMQContainer's usage examples @@ -43,14 +43,14 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:activemq:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-activemq:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - activemq + testcontainers-activemq {{latest_version}} test diff --git a/docs/modules/azure.md b/docs/modules/azure.md index 5e80270e90c..461fcc9f679 100644 --- a/docs/modules/azure.md +++ b/docs/modules/azure.md @@ -5,20 +5,147 @@ This module is INCUBATING. While it is ready for use and operational in the curr Testcontainers module for the Microsoft Azure's [SDK](https://github.com/Azure/azure-sdk-for-java). -Currently, the module supports `CosmosDB` emulator. In order to use it, you should use the following class: +Currently, the module supports `Azurite`, `Azure Event Hubs`, `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. + +Newer Azure Storage SDK versions can send API versions that Azurite does not support. Use `withCommandOptions(...)` to append extra Azurite flags such as `--skipApiVersionCheck`. `AzuriteContainer` rebuilds its process command in `configure()`, so `.withCommand(...)` cannot be used for extra flags. + + +[Pass extra Azurite command options](../../modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerCommandTest.java) inside_block:commandOptions + + +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 a Azure CosmosDB Emulator container](../../modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java) inside_block:emulatorContainer +[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. @@ -51,14 +178,14 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:azure:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-azure:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - azure + testcontainers-azure {{latest_version}} test diff --git a/docs/modules/chromadb.md b/docs/modules/chromadb.md new file mode 100644 index 00000000000..bb50dfe3c02 --- /dev/null +++ b/docs/modules/chromadb.md @@ -0,0 +1,30 @@ +# 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 + +``` diff --git a/docs/modules/consul.md b/docs/modules/consul.md index f9f21bfb541..bcdf7146da3 100644 --- a/docs/modules/consul.md +++ b/docs/modules/consul.md @@ -21,14 +21,14 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:consul:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-consul:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - consul + testcontainers-consul {{latest_version}} test diff --git a/docs/modules/databases/cassandra.md b/docs/modules/databases/cassandra.md index 73f93bf203a..9cc5e12499b 100644 --- a/docs/modules/databases/cassandra.md +++ b/docs/modules/databases/cassandra.md @@ -2,14 +2,36 @@ ## Usage example -This example connects to the Cassandra Cluster, creates a keyspaces and asserts that is has been created. +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: -[Building CqlSession](../../../modules/cassandra/src/test/java/org/testcontainers/containers/CassandraDriver3Test.java) inside_block:cassandra +[SSL setup](../../../modules/cassandra/src/test/java/org/testcontainers/cassandra/CassandraContainerTest.java) inside_block:with-ssl-config -!!! warning - All methods returning instances of the Cassandra Driver's Cluster object in `CassandraContainer` have been deprecated. Providing these methods unnecessarily couples the Container to the Driver and creates potential breaking changes if the driver is updated. +!!! 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 @@ -17,14 +39,14 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:cassandra:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-cassandra:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - cassandra + testcontainers-cassandra {{latest_version}} test diff --git a/docs/modules/databases/clickhouse.md b/docs/modules/databases/clickhouse.md index b3494867edf..4e49c69c276 100644 --- a/docs/modules/databases/clickhouse.md +++ b/docs/modules/databases/clickhouse.md @@ -1,19 +1,35 @@ # 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:clickhouse:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-clickhouse:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - clickhouse + testcontainers-clickhouse {{latest_version}} test diff --git a/docs/modules/databases/cockroachdb.md b/docs/modules/databases/cockroachdb.md index 45e3f75b86c..add1b9cd1aa 100644 --- a/docs/modules/databases/cockroachdb.md +++ b/docs/modules/databases/cockroachdb.md @@ -1,21 +1,37 @@ # 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:cockroachdb:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-cockroachdb:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - cockroachdb + testcontainers-cockroachdb {{latest_version}} test diff --git a/docs/modules/databases/couchbase.md b/docs/modules/databases/couchbase.md index 5c1ff9f5580..997347611e2 100644 --- a/docs/modules/databases/couchbase.md +++ b/docs/modules/databases/couchbase.md @@ -29,13 +29,13 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:couchbase:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-couchbase:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - couchbase + testcontainers-couchbase {{latest_version}} test diff --git a/docs/modules/databases/cratedb.md b/docs/modules/databases/cratedb.md index 74ec23dda00..25fd1642a9d 100644 --- a/docs/modules/databases/cratedb.md +++ b/docs/modules/databases/cratedb.md @@ -1,20 +1,36 @@ # 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:cratedb:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-cratedb:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - cratedb + testcontainers-cratedb {{latest_version}} test diff --git a/docs/modules/databases/databend.md b/docs/modules/databases/databend.md new file mode 100644 index 00000000000..510dffb2f3e --- /dev/null +++ b/docs/modules/databases/databend.md @@ -0,0 +1,40 @@ +# 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. + diff --git a/docs/modules/databases/db2.md b/docs/modules/databases/db2.md index 945536cac05..ce05f6ae308 100644 --- a/docs/modules/databases/db2.md +++ b/docs/modules/databases/db2.md @@ -1,33 +1,25 @@ # DB2 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. - -See [Database containers](./index.md) for documentation and usage that is common to all relational database container types. +Testcontainers module for [DB2](https://www.ibm.com/docs/en/db2/11.5.x?topic=deployments-db2-community-edition-docker) ## Usage example -Running DB2 as a stand-in for in a test: - -```java -public class SomeTest { +You can start a DB2 container instance from any Java application by using: - @ClassRule - public Db2Container db2 = new Db2Container() - .acceptLicense(); - - @Test - public void someTestMethod() { - String url = db2.getJdbcUrl(); - - ... create a connection and run test as normal - } -``` + +[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). - - Please see the [`ibmcom/db2` image documentation](https://hub.docker.com/r/ibmcom/db2) 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:db2:11.5.0.0a:///databasename` + +See [JDBC](./jdbc.md) for documentation. ## Adding this module to your project dependencies @@ -35,13 +27,13 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:db2:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-db2:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - db2 + testcontainers-db2 {{latest_version}} test diff --git a/docs/modules/databases/dynalite.md b/docs/modules/databases/dynalite.md deleted file mode 100644 index 8807ad7f025..00000000000 --- a/docs/modules/databases/dynalite.md +++ /dev/null @@ -1,47 +0,0 @@ -# Dynalite Module - -Testcontainers module for [Dynalite](https://github.com/mhart/dynalite). Dynalite is a clone of DynamoDB, enabling local testing. - -## Usage example - -Running Dynalite as a stand-in for DynamoDB in a test: - -```java -public class SomeTest { - - @Rule - public DynaliteContainer dynamoDB = new DynaliteContainer(); - - @Test - public void someTestMethod() { - // getClient() returns a preconfigured DynamoDB client that is connected to the - // dynalite container - final AmazonDynamoDB client = dynamoDB.getClient(); - - ... interact with client as if using DynamoDB normally -``` - -## Why Dynalite for DynamoDB testing? - -In part, because it's light and quick to run. Also, please see the [reasons given](https://github.com/mhart/dynalite#why-not-amazons-dynamodb-local) by the author of Dynalite and the [problems with Amazon's DynamoDB Local](https://github.com/mhart/dynalite#problems-with-amazons-dynamodb-local-updated-2016-04-19). - -## Adding this module to your project dependencies - -Add the following dependency to your `pom.xml`/`build.gradle` file: - -=== "Gradle" - ```groovy - testImplementation "org.testcontainers:dynalite:{{latest_version}}" - ``` -=== "Maven" - ```xml - - org.testcontainers - dynalite - {{latest_version}} - test - - ``` - -!!! hint - Adding this Testcontainers library JAR will not automatically add an AWS SDK JAR to your project. You should ensure that your project also has a suitable AWS SDK JAR as a dependency. diff --git a/docs/modules/databases/influxdb.md b/docs/modules/databases/influxdb.md index 8055f6aed57..9644a5eb92b 100644 --- a/docs/modules/databases/influxdb.md +++ b/docs/modules/databases/influxdb.md @@ -90,7 +90,7 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy -testImplementation "org.testcontainers:influxdb:{{latest_version}}" +testImplementation "org.testcontainers:testcontainers-influxdb:{{latest_version}}" ``` === "Maven" @@ -99,7 +99,7 @@ testImplementation "org.testcontainers:influxdb:{{latest_version}}" org.testcontainers - influxdb + testcontainers-influxdb {{latest_version}} test diff --git a/docs/modules/databases/jdbc.md b/docs/modules/databases/jdbc.md index dc11954153a..72539aef44e 100644 --- a/docs/modules/databases/jdbc.md +++ b/docs/modules/databases/jdbc.md @@ -37,11 +37,11 @@ Insert `tc:` after `jdbc:` as follows. Note that the hostname, port and database #### Using CrateDB -`jdbc:tc:cratedb:5.2.3//localhost:5432/crate` +`jdbc:tc:cratedb:5.2.3:///databasename` #### Using DB2 -`jdbc:tc:db2:11.5.0.0a//localhost:5432/crate` +`jdbc:tc:db2:11.5.0.0a:///databasename` #### Using MariaDB @@ -55,6 +55,10 @@ Insert `tc:` after `jdbc:` as follows. Note that the hostname, port and database `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` @@ -75,10 +79,18 @@ Insert `tc:` after `jdbc:` as follows. Note that the hostname, port and database `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` @@ -123,7 +135,7 @@ By default database container is being stopped as soon as last connection is clo `jdbc:tc:mysql:8.0.36:///databasename?TC_DAEMON=true` -With this parameter database container will keep running even when there're no open connections. +With this parameter database container will keep running even when there's no open connections. ### Running container with tmpfs options diff --git a/docs/modules/databases/mariadb.md b/docs/modules/databases/mariadb.md index 25367d629bd..d90f9381cb5 100644 --- a/docs/modules/databases/mariadb.md +++ b/docs/modules/databases/mariadb.md @@ -1,7 +1,23 @@ # 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. @@ -14,13 +30,13 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:mariadb:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-mariadb:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - mariadb + testcontainers-mariadb {{latest_version}} test diff --git a/docs/modules/databases/mongodb.md b/docs/modules/databases/mongodb.md index 9e81e28c39f..b861d83bb2c 100644 --- a/docs/modules/databases/mongodb.md +++ b/docs/modules/databases/mongodb.md @@ -1,20 +1,24 @@ # MongoDB 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. +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 +### Usage example The following example shows how to create a MongoDBContainer: -[Creating a MongoDB container](../../../modules/mongodb/src/test/java/org/testcontainers/containers/MongoDBContainerTest.java) inside_block:creatingMongoDBContainer +[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/containers/MongoDBContainerTest.java) inside_block:startingMongoDBContainer +[Starting a MongoDB container](../../../modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBContainerTest.java) inside_block:startingMongoDBContainer !!! note @@ -36,26 +40,57 @@ For instance, to initialize a single node replica set on fixed ports via Docker, 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:mongodb:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-mongodb:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - mongodb + 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 + 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 diff --git a/docs/modules/databases/mssqlserver.md b/docs/modules/databases/mssqlserver.md index 9d0912f2563..5c9d7b0a9fb 100644 --- a/docs/modules/databases/mssqlserver.md +++ b/docs/modules/databases/mssqlserver.md @@ -1,43 +1,41 @@ # MS SQL Server Module -See [Database containers](./index.md) for documentation and usage that is common to all relational database container types. +Testcontainers module for [MS SQL Server](https://mcr.microsoft.com/en-us/artifact/mar/mssql/server/) ## Usage example -Running MS SQL Server as a stand-in for in a test: - -```java -public class SomeTest { +You can start a MS SQL Server container instance from any Java application by using: - @Rule - public MSSQLServerContainer mssqlserver = new MSSQLServerContainer() - .acceptLicense(); - - @Test - public void someTestMethod() { - String url = mssqlserver.getJdbcUrl(); - - ... create a connection and run test as normal -``` + +[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:mssqlserver:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-mssqlserver:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - mssqlserver + testcontainers-mssqlserver {{latest_version}} test diff --git a/docs/modules/databases/mysql.md b/docs/modules/databases/mysql.md index 9d03d52b109..1747989766f 100644 --- a/docs/modules/databases/mysql.md +++ b/docs/modules/databases/mysql.md @@ -1,7 +1,23 @@ # 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` @@ -24,13 +40,13 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:mysql:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-mysql:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - mysql + testcontainers-mysql {{latest_version}} test diff --git a/docs/modules/databases/neo4j.md b/docs/modules/databases/neo4j.md index 17c58c7edff..c7b06e99fe7 100644 --- a/docs/modules/databases/neo4j.md +++ b/docs/modules/databases/neo4j.md @@ -9,19 +9,12 @@ 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: +You can start a Neo4j container instance from any Java application by using: -[JUnit 5 example](../../../examples/neo4j-container/src/test/java/org/testcontainers/containers/Neo4jExampleTest.java) inside_block:junitExample +[Neo4j container](../../../modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java) inside_block:container -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 @@ -29,7 +22,7 @@ You are not limited to Unit tests, and you can use an instance of the Neo4j Test A custom password can be provided: -[Custom password](../../../modules/neo4j/src/test/java/org/testcontainers/containers/Neo4jContainerTest.java) inside_block:withAdminPassword +[Custom password](../../../modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java) inside_block:withAdminPassword ### Disable authentication @@ -37,7 +30,7 @@ A custom password can be provided: Authentication can be disabled: -[Disable authentication](../../../modules/neo4j/src/test/java/org/testcontainers/containers/Neo4jContainerTest.java) inside_block:withoutAuthentication +[Disable authentication](../../../modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java) inside_block:withoutAuthentication ### Random password @@ -45,7 +38,7 @@ Authentication can be disabled: A random (`UUID`-random based) password can be set: -[Random password](../../../modules/neo4j/src/test/java/org/testcontainers/containers/Neo4jContainerTest.java) inside_block:withRandomPassword +[Random password](../../../modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java) inside_block:withRandomPassword ### Neo4j-Configuration @@ -54,7 +47,7 @@ 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/containers/Neo4jContainerTest.java) inside_block:neo4jConfiguration +[Neo4j configuration](../../../modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java) inside_block:neo4jConfiguration ### Add custom plugins @@ -62,27 +55,26 @@ The container takes care of that, and you can configure the database with standa 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/containers/Neo4jContainerTest.java) inside_block:registerPluginsJar +[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/containers/Neo4jContainerTest.java) inside_block:registerPluginsPath +[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 Docker Labs plugin list](https://neo4j.com/docs/operations-manual/4.4/docker/operations/#docker-neo4jlabs-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 - At the moment only the plugins available from the list Neo4j Docker 4.4 are supported by type. - If you want to register another supported Neo4j Labs plugin, you have to add it manually - by using the method `withLabsPlugins(String... neo4jLabsPlugins)`. - Please refer to the list of [supported Docker image plugins](https://neo4j.com/docs/operations-manual/current/docker/operations/#docker-neo4jlabs-plugins). + 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/containers/Neo4jContainerTest.java) inside_block:configureLabsPlugins +[Configure Neo4j Labs Plugins](../../../modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java) inside_block:configureLabsPlugins @@ -91,7 +83,7 @@ Add any Neo4j Labs plugin from the [Neo4j Docker Labs plugin list](https://neo4j 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/containers/Neo4jContainerTest.java) inside_block:copyDatabase +[Copy database](../../../modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java) inside_block:copyDatabase !!! note @@ -102,7 +94,7 @@ If you have an existing database (`graph.db`) you want to work with, copy it ove If you need the Neo4j enterprise license, you can declare your Neo4j container like this: -[Enterprise edition](../../../modules/neo4j/src/test/java/org/testcontainers/containers/Neo4jContainerTest.java) inside_block:enterpriseEdition +[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. @@ -121,13 +113,13 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:neo4j:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-neo4j:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - neo4j + testcontainers-neo4j {{latest_version}} test diff --git a/docs/modules/databases/oceanbase.md b/docs/modules/databases/oceanbase.md new file mode 100644 index 00000000000..c315a871058 --- /dev/null +++ b/docs/modules/databases/oceanbase.md @@ -0,0 +1,41 @@ +# 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. diff --git a/docs/modules/databases/oraclefree.md b/docs/modules/databases/oraclefree.md index c85d672f140..75f3d677ffc 100644 --- a/docs/modules/databases/oraclefree.md +++ b/docs/modules/databases/oraclefree.md @@ -1,27 +1,36 @@ # Oracle Database Free Module -See [Database containers](./index.md) for documentation and usage that is common to all relational database container types. +Testcontainers module for [Oracle Free](https://hub.docker.com/r/gvenzl/oracle-free) ## Usage example -You can use `OracleContainer` like any other JDBC container: +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:constructor +[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:oracle-free:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-oracle-free:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - oracle-free + testcontainers-oracle-free {{latest_version}} test diff --git a/docs/modules/databases/oraclexe.md b/docs/modules/databases/oraclexe.md index bfc06d9a106..8d060ed65be 100644 --- a/docs/modules/databases/oraclexe.md +++ b/docs/modules/databases/oraclexe.md @@ -1,27 +1,36 @@ # Oracle-XE Module -See [Database containers](./index.md) for documentation and usage that is common to all relational database container types. +Testcontainers module for [Oracle XE](https://hub.docker.com/r/gvenzl/oracle-xe) ## Usage example -You can use `OracleContainer` like any other JDBC container: +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:constructor +[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:oracle-xe:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-oracle-xe:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - oracle-xe + testcontainers-oracle-xe {{latest_version}} test diff --git a/docs/modules/databases/orientdb.md b/docs/modules/databases/orientdb.md index 57839cd99f0..f3be7e2bbdb 100644 --- a/docs/modules/databases/orientdb.md +++ b/docs/modules/databases/orientdb.md @@ -1,46 +1,14 @@ # OrientDB 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. - - -This module helps running [OrientDB](https://orientdb.org/download) using Testcontainers. - -Note that it's based on the [official Docker image](https://hub.docker.com/_/orientdb/) provided by OrientDB. +Testcontainers module for [OrientDB](https://hub.docker.com/_/orientdb/) ## Usage example -Declare your Testcontainers instance 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 call `getDbUrl()` OrientDB container and build the `ODatabaseSession` by your own, but a more useful `getSession()` method is provided. -On the JVM you would most likely use the [Java driver](https://github.com/). - -The following example uses the JUnit 5 extension `@Testcontainers` and demonstrates both the usage of the Java Client: - -=== "JUnit 5 example" - ```java - @Testcontainers - public class ExampleTest { - - @Container - private static OrientDBContainer container = new OrientDBContainer(); - - @Test - void testDbCreation() { - - final ODatabaseSession session = container.getSession(); - - 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); - } - - } - ``` - -You are not limited to Unit tests and can of course use an instance of the OrientDB Testcontainers implementation in vanilla Java code as well. +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 @@ -48,13 +16,13 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:orientdb:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-orientdb:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - orientdb + testcontainers-orientdb {{latest_version}} test diff --git a/docs/modules/databases/postgres.md b/docs/modules/databases/postgres.md index b7632d78293..f27752b8354 100644 --- a/docs/modules/databases/postgres.md +++ b/docs/modules/databases/postgres.md @@ -1,20 +1,61 @@ # 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:postgresql:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-postgresql:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - postgresql + testcontainers-postgresql {{latest_version}} test diff --git a/docs/modules/databases/presto.md b/docs/modules/databases/presto.md index d0a47fc1d7a..4e9a6e7c95d 100644 --- a/docs/modules/databases/presto.md +++ b/docs/modules/databases/presto.md @@ -69,13 +69,13 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:presto:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-presto:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - presto + testcontainers-presto {{latest_version}} test diff --git a/docs/modules/databases/questdb.md b/docs/modules/databases/questdb.md index 8783dd4b757..eb5c466f371 100644 --- a/docs/modules/databases/questdb.md +++ b/docs/modules/databases/questdb.md @@ -1,11 +1,24 @@ # QuestDB Module -Testcontainers module for [QuestDB](https://github.com/questdb/questdb). QuestDB is a high-performance, open-source SQL -database for applications in financial services, IoT, machine learning, DevOps and observability. +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: @@ -13,7 +26,7 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy -testImplementation "org.testcontainers:questdb:{{latest_version}}" +testImplementation "org.testcontainers:testcontainers-questdb:{{latest_version}}" ``` === "Maven" @@ -22,7 +35,7 @@ testImplementation "org.testcontainers:questdb:{{latest_version}}" org.testcontainers - questdb + testcontainers-questdb {{latest_version}} test diff --git a/docs/modules/databases/r2dbc.md b/docs/modules/databases/r2dbc.md index 22c76e83dd8..91580811887 100644 --- a/docs/modules/databases/r2dbc.md +++ b/docs/modules/databases/r2dbc.md @@ -12,7 +12,7 @@ As long as you have Testcontainers and the appropriate R2DBC driver on your clas The started container will be terminated when the `ConnectionFactory` is closed. !!! warning - Both the database module (e.g. `org.testcontainers:mysql`) **and** `org.testcontainers:r2dbc` need to be on your application's classpath at runtime. + 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` @@ -33,6 +33,10 @@ So that the URL becomes: ### 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` diff --git a/docs/modules/databases/scylladb.md b/docs/modules/databases/scylladb.md new file mode 100644 index 00000000000..bde40e3b599 --- /dev/null +++ b/docs/modules/databases/scylladb.md @@ -0,0 +1,58 @@ +# 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 + + ``` diff --git a/docs/modules/databases/tidb.md b/docs/modules/databases/tidb.md index 01c6b4cfcd4..17cb66b21c3 100644 --- a/docs/modules/databases/tidb.md +++ b/docs/modules/databases/tidb.md @@ -1,21 +1,37 @@ # 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:tidb:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-tidb:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - tidb + testcontainers-tidb {{latest_version}} test diff --git a/docs/modules/databases/timeplus.md b/docs/modules/databases/timeplus.md new file mode 100644 index 00000000000..ee5e5767223 --- /dev/null +++ b/docs/modules/databases/timeplus.md @@ -0,0 +1,40 @@ +# 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. + diff --git a/docs/modules/databases/trino.md b/docs/modules/databases/trino.md index b6aeddafb40..81fb4ea5b98 100644 --- a/docs/modules/databases/trino.md +++ b/docs/modules/databases/trino.md @@ -1,67 +1,22 @@ # Trino 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. - -See [Database containers](./index.md) for documentation and usage that is common to all database container types. +Testcontainers module for [Trino](https://hub.docker.com/r/trinodb/trino) ## Usage example -Running Trino as a stand-in for in a test: - -```java -public class SomeTest { - - @Rule - public TrinoContainer trino = new TrinoContainer(); - - @Test - public void someTestMethod() { - String url = trino.getJdbcUrl(); +You can start a Trino container instance from any Java application by using: - ... create a connection and run test as normal -``` + +[Container creation](../../../modules/trino/src/test/java/org/testcontainers/trino/TrinoContainerTest.java) inside_block:container + -Trino comes with several catalogs preconfigured. Most useful ones for testing are - -* `tpch` catalog using the [Trino TPCH Connector](https://trino.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 [Trino Memory Connector](https://trino.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: +See [Database containers](./index.md) for documentation and usage that is common to all database container types. -```java -public class SomeTest { - @Rule - public TrinoContainer trino = new TrinoContainer(); +### Testcontainers JDBC URL - @Test - public void queryMemoryAndTpchConnectors() throws SQLException { - try (Connection connection = trino.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"); +`jdbc:tc:trino:352:///defaultname` - // Query Trino 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); - } - } - } -} -``` +See [JDBC](./jdbc.md) for documentation. ## Adding this module to your project dependencies @@ -69,14 +24,14 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:trino:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-trino:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - trino + testcontainers-trino {{latest_version}} test diff --git a/docs/modules/databases/yugabytedb.md b/docs/modules/databases/yugabytedb.md index 4ab3436b57c..4409712241b 100644 --- a/docs/modules/databases/yugabytedb.md +++ b/docs/modules/databases/yugabytedb.md @@ -1,11 +1,11 @@ # YugabyteDB 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 [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. +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 @@ -17,11 +17,11 @@ YugabyteDB supports two APIs. [Creating a YSQL container](../../../modules/yugabytedb/src/test/java/org/testcontainers/junit/yugabytedb/YugabyteDBYSQLTest.java) inside_block:creatingYSQLContainer +### Testcontainers JDBC URL - -[Starting a YSQL container](../../../modules/yugabytedb/src/test/java/org/testcontainers/junit/yugabytedb/YugabyteDBYSQLTest.java) inside_block:startingYSQLContainer - +`jdbc:tc:yugabyte:2.14.4.0-b26:///databasename` +See [JDBC](./jdbc.md) for documentation. ### YCQL API @@ -30,24 +30,19 @@ YugabyteDB supports two APIs. - -[Starting a YCQL container](../../../modules/yugabytedb/src/test/java/org/testcontainers/junit/yugabytedb/YugabyteDBYCQLTest.java) inside_block:startingYCQLContainer - - - ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:yugabytedb:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-yugabytedb:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - yugabytedb + testcontainers-yugabytedb {{latest_version}} test diff --git a/docs/modules/docker_compose.md b/docs/modules/docker_compose.md index 10f1aee61aa..3d3d7a510c2 100644 --- a/docs/modules/docker_compose.md +++ b/docs/modules/docker_compose.md @@ -2,141 +2,121 @@ ## Benefits -Similar to generic containers support, it's also possible to run a bespoke set of services -specified in a `docker-compose.yml` file. +Similar to generic container support, it's also possible to run a bespoke set of services specified in a +`docker-compose.yml` file. -This is intended to be useful on projects where Docker Compose is already used in dev or other environments to define -services that an application may be dependent upon. +This is especially useful for projects where Docker Compose is already used in development +or other environments to define services that an application may be dependent upon. -Behind the scenes, Testcontainers actually launches a temporary Docker Compose client - in a container, of course, so -it's not necessary to have it installed on all developer/test machines. +The `ComposeContainer` leverages [Compose V2](https://www.docker.com/blog/announcing-compose-v2-general-availability/), +making it easy to use the same dependencies from the development environment within tests. ## Example -A single class rule, pointing to a `docker-compose.yml` file, should be sufficient to launch any number of services -required by your tests: -```java -@ClassRule -public static DockerComposeContainer environment = - new DockerComposeContainer(new File("src/test/resources/compose-test.yml")) - .withExposedService("redis_1", REDIS_PORT) - .withExposedService("elasticsearch_1", ELASTICSEARCH_PORT); -``` +A single class `ComposeContainer`, defined based on a `docker-compose.yml` file, +should be sufficient to launch any number of services required by our tests: + + +[Create a ComposeContainer](../../core/src/test/java/org/testcontainers/junit/ComposeContainerTest.java) inside_block:composeContainerConstructor + + +!!! note + Make sure the service names use a `-` rather than `_` as separator. -In this example, `compose-test.yml` should have content such as: +In this example, Docker Compose file should have content such as: ```yaml -redis: - image: redis -elasticsearch: - image: elasticsearch +services: + redis: + image: redis + db: + image: mysql:8.0.36 ``` -Note that it is not necessary to define ports to be exposed in the YAML file; this would inhibit reuse/inclusion of the -file in other contexts. +Note that it is not necessary to define ports to be exposed in the YAML file, +as this would inhibit the reuse/inclusion of the file in other contexts. + +Instead, Testcontainers will spin up a small `ambassador` container, +which will proxy between the Compose-managed containers and ports that are accessible to our tests. + +## ComposeContainer vs DockerComposeContainer -Instead, Testcontainers will spin up a small 'ambassador' container, which will proxy -between the Compose-managed containers and ports that are accessible to your tests. This is done using a separate, minimal -container that runs socat as a TCP proxy. +So far, we discussed `ComposeContainer`, which supports docker compose [version 2](https://www.docker.com/blog/announcing-compose-v2-general-availability/). -## Accessing a container from tests +On the other hand, `DockerComposeContainer` utilizes Compose V1, which has been marked deprecated by Docker. -The rule provides methods for discovering how your tests can interact with the containers: +The two APIs are quite similar, and most examples provided on this page can be applied to both of them. + +## Accessing a Container + +`ComposeContainer` provides methods for discovering how your tests can interact with the containers: * `getServiceHost(serviceName, servicePort)` returns the IP address where the container is listening (via an ambassador container) * `getServicePort(serviceName, servicePort)` returns the Docker mapped port for a port that has been exposed (via an ambassador container) -For example, with the Redis example above, the following will allow your tests to access the Redis service: -```java -String redisUrl = environment.getServiceHost("redis_1", REDIS_PORT) - + ":" + - environment.getServicePort("redis_1", REDIS_PORT); -``` +Let's use this API to create the URL that will enable our tests to access the Redis service: + +[Access a Service's host and port](../../core/src/test/java/org/testcontainers/junit/ComposeContainerTest.java) inside_block:getServiceHostAndPort + -## Startup timeout +## Wait Strategies and Startup Timeouts Ordinarily Testcontainers will wait for up to 60 seconds for each exposed container's first mapped network port to start listening. - This simple measure provides a basic check whether a container is ready for use. -There are overloaded `withExposedService` methods that take a `WaitStrategy` so you can specify a timeout strategy per container. +There are overloaded `withExposedService` methods that take a `WaitStrategy` +where we can specify a timeout strategy per container. -### Waiting for startup examples +We can either use the fluent API to crate a [custom strategy](../features/startup_and_waits.md) or use one of the already existing ones, +accessible via the static factory methods from of the `Wait` class. -Waiting for exposed port to start listening: -```java -@ClassRule -public static DockerComposeContainer environment = - new DockerComposeContainer(new File("src/test/resources/compose-test.yml")) - .withExposedService("redis_1", REDIS_PORT, - Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30))); -``` +For instance, we can wait for exposed port and set a custom timeout: + +[Wait for the exposed port and use a custom timeout](../../core/src/test/java/org/testcontainers/junit/ComposeContainerWithWaitStrategiesTest.java) inside_block:composeContainerWaitForPortWithTimeout + -Wait for arbitrary status codes on an HTTPS endpoint: -```java -@ClassRule -public static DockerComposeContainer environment = - new DockerComposeContainer(new File("src/test/resources/compose-test.yml")) - .withExposedService("elasticsearch_1", ELASTICSEARCH_PORT, - Wait.forHttp("/all") - .forStatusCode(200) - .forStatusCode(401) - .usingTls()); -``` +Needless to say, we can define different strategies for each service in our Docker Compose setup. -Separate wait strategies for each container: -```java -@ClassRule -public static DockerComposeContainer environment = - new DockerComposeContainer(new File("src/test/resources/compose-test.yml")) - .withExposedService("redis_1", REDIS_PORT, Wait.forListeningPort()) - .withExposedService("elasticsearch_1", ELASTICSEARCH_PORT, - Wait.forHttp("/all") - .forStatusCode(200) - .forStatusCode(401) - .usingTls()); -``` +For example, our Redis container can wait for a successful redis-cli command, +while our db service waits for a specific log message: -Alternatively, you can use `waitingFor(serviceName, waitStrategy)`, -for example if you need to wait on a log message from a service, but don't need to expose a port. + +[Wait for a custom command and a log message](../../core/src/test/java/org/testcontainers/junit/ComposeContainerWithWaitStrategiesTest.java) inside_block:composeContainerWithCombinedWaitStrategies + -```java -@ClassRule -public static DockerComposeContainer environment = - new DockerComposeContainer(new File("src/test/resources/compose-test.yml")) - .withExposedService("redis_1", REDIS_PORT, Wait.forListeningPort()) - .waitingFor("db_1", Wait.forLogMessage("started", 1)); -``` -## 'Local compose' mode -You can override Testcontainers' default behaviour and make it use a `docker-compose` binary installed on the local machine. -This will generally yield an experience that is closer to running docker-compose locally, with the caveat that Docker Compose needs to be present on dev and CI machines. -```java -public static DockerComposeContainer environment = - new DockerComposeContainer(new File("src/test/resources/compose-test.yml")) - .withExposedService("redis_1", REDIS_PORT, Wait.forListeningPort()) - .waitingFor("db_1", Wait.forLogMessage("started", 1)) - .withLocalCompose(true); -``` +## The 'Local Compose' Mode -## Compose V2 +We can override Testcontainers' default behaviour and make it use a `docker-compose` binary installed on the local machine. -[Compose V2 is GA](https://www.docker.com/blog/announcing-compose-v2-general-availability/) and it relies on the `docker` command itself instead of `docker-compose`. -Testcontainers provides `ComposeContainer` if you want to use Compose V2. +This will generally yield an experience that is closer to running _docker compose_ locally, +with the caveat that Docker Compose needs to be present on dev and CI machines. -```java -public static ComposeContainer environment = - new ComposeContainer(new File("src/test/resources/compose-test.yml")) - .withExposedService("redis-1", REDIS_PORT, Wait.forListeningPort()) - .waitingFor("db-1", Wait.forLogMessage("started", 1)); -``` + +[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 - Make sure the service name use a `-` instead of `_` as separator using `ComposeContainer`. + This can be used with `DockerComposeContainer` and `ComposeContainer`, but **only in the containerized Compose (not with `Local Compose` mode)**. ## Using private repositories in Docker compose -When Docker Compose is used in container mode (not local), it's needs to be made aware of Docker settings for private repositories. +When Docker Compose is used in container mode (not local), it needs to be made aware of Docker +settings for private repositories. By default, those setting are located in `$HOME/.docker/config.json`. There are 3 ways to specify location of the `config.json` for Docker Compose: diff --git a/docs/modules/docker_mcp_gateway.md b/docs/modules/docker_mcp_gateway.md new file mode 100644 index 00000000000..af526bab3fe --- /dev/null +++ b/docs/modules/docker_mcp_gateway.md @@ -0,0 +1,32 @@ +# 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 + + ``` + diff --git a/docs/modules/docker_model_runner.md b/docs/modules/docker_model_runner.md new file mode 100644 index 00000000000..b610279e93b --- /dev/null +++ b/docs/modules/docker_model_runner.md @@ -0,0 +1,41 @@ +# 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 + + ``` + diff --git a/docs/modules/elasticsearch.md b/docs/modules/elasticsearch.md index 3372d2fdea0..5817a21de02 100644 --- a/docs/modules/elasticsearch.md +++ b/docs/modules/elasticsearch.md @@ -10,7 +10,9 @@ Note that it's based on the [official Docker image](https://www.elastic.co/guide 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:httpClientContainer +[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 @@ -28,20 +30,47 @@ You can turn on security by providing a password: [HttpClient](../../modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/ElasticsearchContainerTest.java) inside_block:httpClientSecuredContainer +## Kibana container + +This module also provides a `KibanaContainer` for testing with [Kibana](https://www.elastic.co/kibana). +Kibana requires a connection to Elasticsearch and `KibanaContainer` supports two modes: managed and external. + +### Managed mode + +In managed mode, `KibanaContainer` automatically connects to an `ElasticsearchContainer`: + + +[Kibana with Elasticsearch](../../modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/KibanaContainerTest.java) inside_block:managedModeCanStartAndReachElasticsearchInSameExplicitNetwork + + +When using managed mode with explicit networks, both containers must share the same `Network` instance. +Alternatively, you can omit the network configuration entirely, and `KibanaContainer` will do its best effort to create a shared, ad-hoc network automatically. + +### External mode + +In external mode, `KibanaContainer` connects to an external Elasticsearch instance via URL and using provided credentials: + + +[Kibana with external Elasticsearch](../../modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/KibanaContainerTest.java) inside_block:externalModeCanWorkWithUsernamePassword + + +For external mode with HTTPS, use `withElasticsearchCaCertificate()` to provide the CA certificate. +You can authenticate using either username/password (`withElasticsearchCredentials()`) or service account tokens (`withElasticsearchServiceAccountToken()`). + ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:elasticsearch:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-elasticsearch:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - elasticsearch + testcontainers-elasticsearch {{latest_version}} test diff --git a/docs/modules/gcloud.md b/docs/modules/gcloud.md index 31542a5d387..9660461d5a0 100644 --- a/docs/modules/gcloud.md +++ b/docs/modules/gcloud.md @@ -23,11 +23,11 @@ PubSubEmulatorContainer | [gcr.io/google.com/cloudsdktool/google-cloud-cli:emula Start BigQuery Emulator during a test: -[Starting a BigQuery Emulator container](../../modules/gcloud/src/test/java/org/testcontainers/containers/BigQueryEmulatorContainerTest.java) inside_block:emulatorContainer +[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/containers/BigQueryEmulatorContainerTest.java) inside_block:bigQueryClient +[Creating BigQuery Client](../../modules/gcloud/src/test/java/org/testcontainers/gcloud/BigQueryEmulatorContainerTest.java) inside_block:bigQueryClient ### Bigtable @@ -35,19 +35,19 @@ Start BigQuery Emulator during a test: Start Bigtable Emulator during a test: -[Starting a Bigtable Emulator container](../../modules/gcloud/src/test/java/org/testcontainers/containers/BigtableEmulatorContainerTest.java) inside_block:emulatorContainer +[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/containers/BigtableEmulatorContainerTest.java) inside_block:createTable +[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/containers/BigtableEmulatorContainerTest.java) inside_block:testWithEmulatorContainer +[Testing with a Bigtable Emulator container](../../modules/gcloud/src/test/java/org/testcontainers/gcloud/BigtableEmulatorContainerTest.java) inside_block:testWithEmulatorContainer ### Datastore @@ -55,18 +55,18 @@ Test against the Emulator: Start Datastore Emulator during a test: -[Starting a Datastore Emulator container](../../modules/gcloud/src/test/java/org/testcontainers/containers/DatastoreEmulatorContainerTest.java) inside_block:creatingDatastoreEmulatorContainer +[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/containers/DatastoreEmulatorContainerTest.java) inside_block:startingDatastoreEmulatorContainer +[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/containers/DatastoreEmulatorContainerTest.java) + * [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 @@ -74,18 +74,18 @@ See more examples: Start Firestore Emulator during a test: -[Starting a Firestore Emulator container](../../modules/gcloud/src/test/java/org/testcontainers/containers/FirestoreEmulatorContainerTest.java) inside_block:emulatorContainer +[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/containers/FirestoreEmulatorContainerTest.java) inside_block:testWithEmulatorContainer +[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/containers/FirestoreEmulatorContainerTest.java) + * [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 @@ -93,30 +93,30 @@ See more examples: Start Spanner Emulator during a test: -[Starting a Spanner Emulator container](../../modules/gcloud/src/test/java/org/testcontainers/containers/SpannerEmulatorContainerTest.java) inside_block:emulatorContainer +[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/containers/SpannerEmulatorContainerTest.java) inside_block:createInstance +[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/containers/SpannerEmulatorContainerTest.java) inside_block:createDatabase +[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 Firestore Emulator container](../../modules/gcloud/src/test/java/org/testcontainers/containers/SpannerEmulatorContainerTest.java) inside_block:testWithEmulatorContainer +[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/containers/SpannerEmulatorContainerTest.java) + * [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 @@ -124,30 +124,30 @@ See more examples: Start Pub/Sub Emulator during a test: -[Starting a Pub/Sub Emulator container](../../modules/gcloud/src/test/java/org/testcontainers/containers/PubSubEmulatorContainerTest.java) inside_block:emulatorContainer +[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/containers/PubSubEmulatorContainerTest.java) inside_block:createTopic +[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/containers/PubSubEmulatorContainerTest.java) inside_block:createSubscription +[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/containers/PubSubEmulatorContainerTest.java) inside_block:testWithEmulatorContainer +[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/containers/PubSubEmulatorContainerTest.java) + * [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 @@ -156,13 +156,13 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:gcloud:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-gcloud:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - gcloud + testcontainers-gcloud {{latest_version}} test diff --git a/docs/modules/grafana.md b/docs/modules/grafana.md new file mode 100644 index 00000000000..397ade89ee3 --- /dev/null +++ b/docs/modules/grafana.md @@ -0,0 +1,30 @@ +# 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 + + ``` diff --git a/docs/modules/hivemq.md b/docs/modules/hivemq.md index 2ebdd7a9ad8..8bba4c18b36 100644 --- a/docs/modules/hivemq.md +++ b/docs/modules/hivemq.md @@ -1,8 +1,8 @@ # HiveMQ Module -drawing +![hivemq logo](../modules_logos/hivemq-module.png) -Automatic starting HiveMQ docker containers for JUnit4 and JUnit5 tests. +Automatic starting HiveMQ docker containers for JUnit5 tests. This enables testing MQTT client applications and integration testing of custom HiveMQ extensions. - Community forum: https://community.hivemq.com/ @@ -212,13 +212,13 @@ can be customized as desired. Add to `build.gradle`: ````groovy -testImplementation 'org.testcontainers:hivemq:{{latest_version}}' +testImplementation 'org.testcontainers:testcontainers-hivemq:{{latest_version}}' ```` Add to `build.gradle.kts`: ````kotlin -testImplementation("org.testcontainers:hivemq:{{latest_version}}") +testImplementation("org.testcontainers:testcontainers-hivemq:{{latest_version}}") ```` ### Maven @@ -228,7 +228,7 @@ Add to `pom.xml`: ```xml org.testcontainers - hivemq + testcontainers-hivemq {{latest_version}} test diff --git a/docs/modules/k3s.md b/docs/modules/k3s.md index 2df695b1c1e..a9de1b3fd4f 100644 --- a/docs/modules/k3s.md +++ b/docs/modules/k3s.md @@ -44,13 +44,13 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:k3s:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-k3s:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - k3s + testcontainers-k3s {{latest_version}} test diff --git a/docs/modules/k6.md b/docs/modules/k6.md new file mode 100644 index 00000000000..13e0ddc9252 --- /dev/null +++ b/docs/modules/k6.md @@ -0,0 +1,50 @@ +# 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 + + ``` diff --git a/docs/modules/kafka.md b/docs/modules/kafka.md index fb953716225..cb2906265d0 100644 --- a/docs/modules/kafka.md +++ b/docs/modules/kafka.md @@ -1,55 +1,71 @@ -# Kafka Containers +# Kafka Module Testcontainers can be used to automatically instantiate and manage [Apache Kafka](https://kafka.apache.org) containers. -More precisely Testcontainers uses the official Docker images for [Confluent OSS Platform](https://hub.docker.com/r/confluentinc/cp-kafka/) + +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. But see [below](#zookeeper) +* 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/containers/KafkaContainerTest.java) inside_block:constructorWithVersion +[Creating a KafkaContainer](../../modules/kafka/src/test/java/org/testcontainers/kafka/KafkaContainerTest.java) inside_block:constructorWithVersion -The correspondence between Confluent Platform versions and Kafka versions can be seen [in Confluent documentation](https://docs.confluent.io/current/installation/versions-interoperability.html#cp-and-apache-kafka-compatibility) - 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 -## Options - -### Using external Zookeeper +### 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: -If for some reason you want to use an externally running Zookeeper, then just pass its location during construction: -[External Zookeeper](../../modules/kafka/src/test/java/org/testcontainers/containers/KafkaContainerTest.java) inside_block:withExternalZookeeper +[Creating a ConfluentKafkaContainer](../../modules/kafka/src/test/java/org/testcontainers/kafka/ConfluentKafkaContainerTest.java) inside_block:constructorWithVersion +## Options + ### Using Kraft mode -KRaft mode was declared production ready in 3.3.1 (confluentinc/cp-kafka:7.3.x)" +!!! 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. +See the [versions interoperability matrix](https://docs.confluent.io/platform/current/installation/versions-interoperability.html) for more details. -## Register listeners +### 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 `9093`. E.g [Toxiproxy](../../modules/toxiproxy/). +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/containers/KafkaContainerTest.java) inside_block:registerListener +[Register additional listener](../../modules/kafka/src/test/java/org/testcontainers/kafka/KafkaContainerTest.java) inside_block:registerListener Container defined in the same network: @@ -70,13 +86,13 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:kafka:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-kafka:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - kafka + testcontainers-kafka {{latest_version}} test diff --git a/docs/modules/ldap.md b/docs/modules/ldap.md new file mode 100644 index 00000000000..c84469f22ae --- /dev/null +++ b/docs/modules/ldap.md @@ -0,0 +1,30 @@ +# 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 + + ``` diff --git a/docs/modules/localstack.md b/docs/modules/localstack.md index b6c82ccf67a..ca51979f5a0 100644 --- a/docs/modules/localstack.md +++ b/docs/modules/localstack.md @@ -4,48 +4,26 @@ Testcontainers module for [LocalStack](http://localstack.cloud/), 'a fully funct ## Usage example -Running LocalStack as a stand-in for AWS S3 during a test: - -```java -DockerImageName localstackImage = DockerImageName.parse("localstack/localstack:0.11.3"); - -@Rule -public LocalStackContainer localstack = new LocalStackContainer(localstackImage) - .withServices(S3); -``` - -## Creating a client using AWS SDK - - -[AWS SDK V1](../../modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackContainerTest.java) inside_block:with_aws_sdk_v1 - +You can start a LocalStack container instance from any Java application by using: -[AWS SDK V2](../../modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackContainerTest.java) inside_block:with_aws_sdk_v2 +[Container creation](../../modules/localstack/src/test/java/org/testcontainers/localstack/LocalStackContainerTest.java) inside_block:container -Environment variables listed in [Localstack's README](https://github.com/localstack/localstack#configurations) may be used to customize Localstack's configuration. +Environment variables listed in the [LocalStack configuration documentation](https://docs.localstack.cloud/references/configuration/) may be used to customize LocalStack's configuration. Use the `.withEnv(key, value)` method on `LocalStackContainer` to apply configuration settings. -## `HOSTNAME_EXTERNAL` and hostname-sensitive services +!!! note + Starting March 23, 2026, `localstack/localstack` requires authentication via a `LOCALSTACK_AUTH_TOKEN` environment variable. Without it, the container will fail to start. -Some Localstack APIs, such as SQS, require the container to be aware of the hostname that it is accessible on - for example, for construction of queue URLs in responses. + Use `.withEnv("LOCALSTACK_AUTH_TOKEN", System.getenv("LOCALSTACK_AUTH_TOKEN"))` to pass the token. + See the [LocalStack blog post](https://blog.localstack.cloud/localstack-single-image-next-steps/) for more details. -Testcontainers will inform Localstack of the best hostname automatically, using the `HOSTNAME_EXTERNAL` environment variable: - -* when running the Localstack container directly without a custom network defined, it is expected that all calls to the container will be from the test host. As such, the container address will be used (typically localhost or the address where the Docker daemon is running). - - - [Localstack container running without a custom network](../../modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackContainerTest.java) inside_block:without_network - - -* when running the Localstack container [with a custom network defined](/features/networking/#advanced-networking), it is expected that all calls to the container will be **from other containers on that network**. `HOSTNAME_EXTERNAL` will be set to the *last* network alias that has been configured for the Localstack container. - - - [Localstack container running with a custom network](../../modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackContainerTest.java) inside_block:with_network - +## Creating a client using AWS SDK -* Other usage scenarios, such as where the Localstack container is used from both the test host and containers on a custom network are not automatically supported. If you have this use case, you should set `HOSTNAME_EXTERNAL` manually. + +[AWS SDK V2](../../modules/localstack/src/test/java/org/testcontainers/localstack/LocalStackContainerTest.java) inside_block:with_aws_sdk_v2 + ## Adding this module to your project dependencies @@ -53,13 +31,13 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:localstack:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-localstack:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - localstack + testcontainers-localstack {{latest_version}} test diff --git a/docs/modules/milvus.md b/docs/modules/milvus.md new file mode 100644 index 00000000000..96772560f80 --- /dev/null +++ b/docs/modules/milvus.md @@ -0,0 +1,36 @@ +# 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 + + ``` diff --git a/docs/modules/minio.md b/docs/modules/minio.md index 2b872682ebe..165f760425c 100644 --- a/docs/modules/minio.md +++ b/docs/modules/minio.md @@ -25,14 +25,14 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:minio:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-minio:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - minio + testcontainers-minio {{latest_version}} test diff --git a/docs/modules/mockserver.md b/docs/modules/mockserver.md index c739278acff..e6a0ee91ece 100644 --- a/docs/modules/mockserver.md +++ b/docs/modules/mockserver.md @@ -7,13 +7,13 @@ Mock Server can be used to mock HTTP services by matching requests against user- The following example shows how to start Mockserver. -[Creating a MockServer container](../../modules/mockserver/src/test/java/org/testcontainers/containers/MockServerContainerRuleTest.java) inside_block:creatingProxy +[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/containers/MockServerContainerRuleTest.java) inside_block:testSimpleExpectation +[Setting a simple expectation](../../modules/mockserver/src/test/java/org/testcontainers/mockserver/MockServerContainerTest.java) inside_block:testSimpleExpectation ## Adding this module to your project dependencies @@ -22,13 +22,13 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:mockserver:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-mockserver:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - mockserver + testcontainers-mockserver {{latest_version}} test diff --git a/docs/modules/nginx.md b/docs/modules/nginx.md index fb4270ba4a3..6fcdbdaf13f 100644 --- a/docs/modules/nginx.md +++ b/docs/modules/nginx.md @@ -7,19 +7,19 @@ Nginx is a web server, reverse proxy and mail proxy and http cache. The following example shows how to start Nginx. -[Creating a Nginx container](../../modules/nginx/src/test/java/org/testcontainers/junit/SimpleNginxTest.java) inside_block:creatingContainer +[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/junit/SimpleNginxTest.java) inside_block:addCustomContent +[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/junit/SimpleNginxTest.java) inside_block:getFromNginxServer +[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 @@ -28,13 +28,13 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:nginx:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-nginx:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - nginx + testcontainers-nginx {{latest_version}} test diff --git a/docs/modules/ollama.md b/docs/modules/ollama.md new file mode 100644 index 00000000000..56c1574d4db --- /dev/null +++ b/docs/modules/ollama.md @@ -0,0 +1,52 @@ +# 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 + + ``` diff --git a/docs/modules/openfga.md b/docs/modules/openfga.md new file mode 100644 index 00000000000..4d916ce6aa6 --- /dev/null +++ b/docs/modules/openfga.md @@ -0,0 +1,30 @@ +# 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 + + ``` diff --git a/docs/modules/pinecone.md b/docs/modules/pinecone.md new file mode 100644 index 00000000000..6bcea81ab39 --- /dev/null +++ b/docs/modules/pinecone.md @@ -0,0 +1,30 @@ +# 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 + +``` diff --git a/docs/modules/pulsar.md b/docs/modules/pulsar.md index f74c315cbdc..5bd33b0bc4c 100644 --- a/docs/modules/pulsar.md +++ b/docs/modules/pulsar.md @@ -9,13 +9,13 @@ It's based on the official Apache Pulsar docker image, it is recommended to read Create a `PulsarContainer` to use it in your tests: -[Create a Pulsar container](../../modules/pulsar/src/test/java/org/testcontainers/containers/PulsarContainerTest.java) inside_block:constructorWithVersion +[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/containers/PulsarContainerTest.java) inside_block:coordinates +[Get broker and admin urls](../../modules/pulsar/src/test/java/org/testcontainers/pulsar/PulsarContainerTest.java) inside_block:coordinates ## Options @@ -26,7 +26,7 @@ If you need to set Pulsar configuration variables you can use the native APIs an For example, if you want to enable `brokerDeduplicationEnabled`: -[Set configuration variables](../../modules/pulsar/src/test/java/org/testcontainers/containers/PulsarContainerTest.java) inside_block:constructorWithEnv +[Set configuration variables](../../modules/pulsar/src/test/java/org/testcontainers/pulsar/PulsarContainerTest.java) inside_block:constructorWithEnv ### Pulsar IO @@ -34,7 +34,7 @@ For example, if you want to enable `brokerDeduplicationEnabled`: 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/containers/PulsarContainerTest.java) inside_block:constructorWithFunctionsWorker +[Create a Pulsar container with functions worker](../../modules/pulsar/src/test/java/org/testcontainers/pulsar/PulsarContainerTest.java) inside_block:constructorWithFunctionsWorker ### Pulsar Transactions @@ -42,7 +42,7 @@ If you need to test Pulsar IO framework you can enable the Pulsar Functions Work 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/containers/PulsarContainerTest.java) inside_block:constructorWithTransactions +[Create a Pulsar container with transactions](../../modules/pulsar/src/test/java/org/testcontainers/pulsar/PulsarContainerTest.java) inside_block:constructorWithTransactions @@ -52,13 +52,13 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:pulsar:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-pulsar:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - pulsar + testcontainers-pulsar {{latest_version}} test diff --git a/docs/modules/qdrant.md b/docs/modules/qdrant.md new file mode 100644 index 00000000000..9431a340a9c --- /dev/null +++ b/docs/modules/qdrant.md @@ -0,0 +1,30 @@ +# 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 + + ``` diff --git a/docs/modules/rabbitmq.md b/docs/modules/rabbitmq.md index 8a9ae9d9b97..5381e3cd910 100644 --- a/docs/modules/rabbitmq.md +++ b/docs/modules/rabbitmq.md @@ -1,21 +1,18 @@ # RabbitMQ 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. - ## Adding this module to your project dependencies Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:rabbitmq:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-rabbitmq:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - rabbitmq + testcontainers-rabbitmq {{latest_version}} test diff --git a/docs/modules/redpanda.md b/docs/modules/redpanda.md index 22545ffb026..429ae5a9768 100644 --- a/docs/modules/redpanda.md +++ b/docs/modules/redpanda.md @@ -45,7 +45,7 @@ Below is an example of how to create the `AdminClient`: 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](../../docs/modules/toxiproxy.md). +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 @@ -63,19 +63,39 @@ 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:redpanda:{{latest_version}}" +testImplementation "org.testcontainers:testcontainers-redpanda:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - redpanda + testcontainers-redpanda {{latest_version}} test diff --git a/docs/modules/solace.md b/docs/modules/solace.md index 93fc0e9875f..91956533906 100644 --- a/docs/modules/solace.md +++ b/docs/modules/solace.md @@ -26,13 +26,13 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:solace:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-solace:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - solace + testcontainers-solace {{latest_version}} test diff --git a/docs/modules/solr.md b/docs/modules/solr.md index 44be46c1a1a..957615fcbf1 100644 --- a/docs/modules/solr.md +++ b/docs/modules/solr.md @@ -1,10 +1,6 @@ # Solr Container -!!! 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. - - -This module helps running [solr](https://lucene.apache.org/solr/) using Testcontainers. +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/). @@ -13,7 +9,7 @@ Note that it's based on the [official Docker image](https://hub.docker.com/_/sol You can start a solr container instance from any Java application by using: -[Using a Solr container](../../modules/solr/src/test/java/org/testcontainers/containers/SolrContainerTest.java) inside_block:solrContainerUsage +[Using a Solr container](../../modules/solr/src/test/java/org/testcontainers/solr/SolrContainerTest.java) inside_block:solrContainerUsage ## Adding this module to your project dependencies @@ -22,13 +18,13 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:solr:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-solr:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - solr + testcontainers-solr {{latest_version}} test diff --git a/docs/modules/toxiproxy.md b/docs/modules/toxiproxy.md index 1b8229982e6..40f792911bb 100644 --- a/docs/modules/toxiproxy.md +++ b/docs/modules/toxiproxy.md @@ -17,7 +17,7 @@ A Toxiproxy container can be placed in between test code and a container, or in 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/containers/ToxiproxyTest.java) inside_block:creatingProxy +[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. @@ -26,13 +26,13 @@ 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/containers/ToxiproxyTest.java) inside_block:obtainProxyObject +[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/containers/ToxiproxyTest.java) inside_block:obtainProxiedHostAndPortForHostMachine +[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. @@ -56,13 +56,13 @@ Please see the [Toxiproxy documentation](https://github.com/Shopify/toxiproxy#to 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/containers/ToxiproxyTest.java) inside_block:addingLatency +[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/containers/ToxiproxyTest.java) inside_block:disableProxy +[Cutting a connection](../../modules/toxiproxy/src/test/java/org/testcontainers/toxiproxy/ToxiproxyContainerTest.java) inside_block:disableProxy ## Adding this module to your project dependencies @@ -71,13 +71,13 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:toxiproxy:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-toxiproxy:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - toxiproxy + testcontainers-toxiproxy {{latest_version}} test @@ -86,5 +86,3 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: ## 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). - -[toxiproxy-java](https://github.com/trekawek/toxiproxy-java) is used to help control failure conditions. diff --git a/docs/modules/typesense.md b/docs/modules/typesense.md new file mode 100644 index 00000000000..c73a640d881 --- /dev/null +++ b/docs/modules/typesense.md @@ -0,0 +1,30 @@ +# Typesense + +Testcontainers module for [Typesense](https://hub.docker.com/r/typesense/typesense). + +## TypesenseContainer's usage examples + +You can start 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 + + ``` diff --git a/docs/modules/vault.md b/docs/modules/vault.md index e32abfd1c60..161efa8ccb0 100644 --- a/docs/modules/vault.md +++ b/docs/modules/vault.md @@ -4,7 +4,7 @@ Testcontainers module for [Vault](https://github.com/hashicorp/vault). Vault is ## Usage example -Start Vault container as a `@ClassRule`: +You can start a Vault container instance from any Java application by using: [Starting a Vault container](../../modules/vault/src/test/java/org/testcontainers/vault/VaultContainerTest.java) inside_block:vaultContainer @@ -22,34 +22,19 @@ 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:vault:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-vault:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - vault + testcontainers-vault {{latest_version}} test diff --git a/docs/modules/weaviate.md b/docs/modules/weaviate.md new file mode 100644 index 00000000000..76146ab5ee9 --- /dev/null +++ b/docs/modules/weaviate.md @@ -0,0 +1,30 @@ +# 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 + +``` diff --git a/docs/modules/webdriver_containers.md b/docs/modules/webdriver_containers.md index 65b39bc5a8f..8101f489e3a 100644 --- a/docs/modules/webdriver_containers.md +++ b/docs/modules/webdriver_containers.md @@ -23,14 +23,14 @@ every test. The following field in your JUnit UI test class will prepare a container running Chrome: -[Chrome](../../modules/selenium/src/test/java/org/testcontainers/junit/ChromeWebDriverContainerTest.java) inside_block:junitRule +[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/junit/LocalServerWebDriverContainerTest.java) inside_block:getWebDriver +[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. @@ -38,7 +38,7 @@ 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/junit/LocalServerWebDriverContainerTest.java) inside_block:getPage +[Open Web Page](../../modules/selenium/src/test/java/org/testcontainers/selenium/LocalServerWebDriverContainerTest.java) inside_block:getPage @@ -48,9 +48,9 @@ running on - which is quite likely), you'll need to use [the host exposing](../f 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/junit/ChromeWebDriverContainerTest.java) inside_block:junitRule -[Firefox](../../modules/selenium/src/test/java/org/testcontainers/junit/FirefoxWebDriverContainerTest.java) inside_block:junitRule -[Edge](../../modules/selenium/src/test/java/org/testcontainers/junit/EdgeWebDriverContainerTest.java) inside_block:junitRule +[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 @@ -59,8 +59,8 @@ By default, no videos will be recorded. However, you can instruct Testcontainers just for failing tests. -[Record all Tests](../../modules/selenium/src/test/java/org/testcontainers/junit/ChromeRecordingWebDriverContainerTest.java) inside_block:recordAll -[Record failing Tests](../../modules/selenium/src/test/java/org/testcontainers/junit/ChromeRecordingWebDriverContainerTest.java) inside_block:recordFailing +[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. @@ -68,13 +68,13 @@ Note that the second parameter of `withRecordingMode` should be a directory wher 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/junit/ChromeRecordingWebDriverContainerTest.java) inside_block:recordMp4 -[Video Format in FLV](../../modules/selenium/src/test/java/org/testcontainers/junit/ChromeRecordingWebDriverContainerTest.java) inside_block:recordFlv +[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/junit/ChromeRecordingWebDriverContainerTest.java) inside_block:withRecordingFileFactory +[CustomRecordingFileFactory](../../modules/selenium/src/test/java/org/testcontainers/selenium/ChromeRecordingWebDriverContainerTest.java) inside_block:withRecordingFileFactory @@ -82,7 +82,7 @@ Note the factory must implement `org.testcontainers.containers.RecordingFileFact ## 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/junit/ChromeWebDriverContainerTest.java). +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 @@ -90,13 +90,13 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:selenium:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-selenium:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - selenium + testcontainers-selenium {{latest_version}} test diff --git a/docs/quickstart/junit_5_quickstart.md b/docs/quickstart/junit_5_quickstart.md index 3c126196845..effd17dd2d9 100644 --- a/docs/quickstart/junit_5_quickstart.md +++ b/docs/quickstart/junit_5_quickstart.md @@ -25,7 +25,7 @@ First, add Testcontainers as a dependency as follows: ```groovy testImplementation "org.junit.jupiter:junit-jupiter:5.8.1" testImplementation "org.testcontainers:testcontainers:{{latest_version}}" - testImplementation "org.testcontainers:junit-jupiter:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-junit-jupiter:{{latest_version}}" ``` === "Maven" ```xml @@ -43,7 +43,7 @@ First, add Testcontainers as a dependency as follows: org.testcontainers - junit-jupiter + testcontainers-junit-jupiter {{latest_version}} test diff --git a/docs/quickstart/spock_quickstart.md b/docs/quickstart/spock_quickstart.md index 267954e99ee..c9c78bea5ea 100644 --- a/docs/quickstart/spock_quickstart.md +++ b/docs/quickstart/spock_quickstart.md @@ -23,13 +23,13 @@ First, add Testcontainers as a dependency as follows: === "Gradle" ```groovy - testImplementation "org.testcontainers:spock:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-spock:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - spock + testcontainers-spock {{latest_version}} test diff --git a/docs/supported_docker_environment/continuous_integration/circle_ci.md b/docs/supported_docker_environment/continuous_integration/circle_ci.md index d6e85a62a79..936192cc799 100644 --- a/docs/supported_docker_environment/continuous_integration/circle_ci.md +++ b/docs/supported_docker_environment/continuous_integration/circle_ci.md @@ -1,7 +1,7 @@ # 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/2.0/executor-types/) for more info). +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: diff --git a/docs/supported_docker_environment/continuous_integration/gitlab_ci.md b/docs/supported_docker_environment/continuous_integration/gitlab_ci.md index 5242e095b56..ccc73f58078 100644 --- a/docs/supported_docker_environment/continuous_integration/gitlab_ci.md +++ b/docs/supported_docker_environment/continuous_integration/gitlab_ci.md @@ -24,10 +24,10 @@ See below for an example runner configuration: Please also include the following in your GitlabCI pipeline definitions (`.gitlab-ci.yml`) that use Testcontainers: ```yml variables: - TESTCONTAINERS_HOST_OVERRIDE: "host.docker.internal" + 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. +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) diff --git a/docs/test_framework_integration/external.md b/docs/test_framework_integration/external.md index b3f55783123..3396ff15d52 100644 --- a/docs/test_framework_integration/external.md +++ b/docs/test_framework_integration/external.md @@ -5,5 +5,6 @@ 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-extensions-testcontainers) | [kotest.io](https://kotest.io/docs/extensions/test_containers.html) | +| 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) | diff --git a/docs/test_framework_integration/junit_5.md b/docs/test_framework_integration/junit_5.md index 15597f90cdb..aed2d515af9 100644 --- a/docs/test_framework_integration/junit_5.md +++ b/docs/test_framework_integration/junit_5.md @@ -1,7 +1,6 @@ # 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. +This module provides an API that is based on the [JUnit Jupiter](https://junit.org/junit5/) extension model. The extension supports two modes: @@ -58,11 +57,7 @@ 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 be tested with sequential test execution. Using it with parallel test execution is unsupported and may have unintended side effects. +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 @@ -70,13 +65,13 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:junit-jupiter:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-junit-jupiter:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - junit-jupiter + testcontainers-junit-jupiter {{latest_version}} test diff --git a/docs/test_framework_integration/manual_lifecycle_control.md b/docs/test_framework_integration/manual_lifecycle_control.md index 6de3cb47196..e1005397c00 100644 --- a/docs/test_framework_integration/manual_lifecycle_control.md +++ b/docs/test_framework_integration/manual_lifecycle_control.md @@ -1,7 +1,6 @@ # 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. +Testcontainers is fully usable with any test framework, or with no framework at all. ## Manually starting/stopping containers diff --git a/docs/test_framework_integration/spock.md b/docs/test_framework_integration/spock.md index b8c1d5b7eae..3d50e0399d2 100644 --- a/docs/test_framework_integration/spock.md +++ b/docs/test_framework_integration/spock.md @@ -19,13 +19,13 @@ Add the following dependency to your `pom.xml`/`build.gradle` file: === "Gradle" ```groovy - testImplementation "org.testcontainers:spock:{{latest_version}}" + testImplementation "org.testcontainers:testcontainers-spock:{{latest_version}}" ``` === "Maven" ```xml org.testcontainers - spock + testcontainers-spock {{latest_version}} test diff --git a/examples/build.gradle b/examples/build.gradle index a83df2bedce..e80815cdae6 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -1,6 +1,6 @@ // empty build.gradle for dependabot plugins { - id 'com.diffplug.spotless' version '6.13.0' apply false + id 'com.diffplug.spotless' version '6.22.0' apply false } apply from: "$rootDir/../gradle/ci-support.gradle" @@ -14,8 +14,18 @@ subprojects { mavenCentral() } + test { + defaultCharacterEncoding = "UTF-8" + testLogging { + displayGranularity 1 + showStackTraces = true + exceptionFormat = 'full' + events "STARTED", "PASSED", "FAILED", "SKIPPED" + } + } + checkstyle { - toolVersion = "9.3" + toolVersion = "10.23.0" configFile = rootProject.file('../config/checkstyle/checkstyle.xml') } } diff --git a/examples/cucumber/build.gradle b/examples/cucumber/build.gradle index 9164a44c9be..84dbc39a85e 100644 --- a/examples/cucumber/build.gradle +++ b/examples/cucumber/build.gradle @@ -7,27 +7,21 @@ repositories { } dependencies { - implementation platform('org.seleniumhq.selenium:selenium-bom:4.17.0') + 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('io.cucumber:cucumber-bom:7.15.0') + 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' - testImplementation 'org.testcontainers:selenium' - testImplementation 'org.assertj:assertj-core:3.25.2' + 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 { - javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(11) - } -} - -compileTestJava { - javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(11) - } - options.release.set(11) + useJUnitPlatform() } diff --git a/examples/cucumber/src/test/java/org/testcontainers/examples/CucumberTest.java b/examples/cucumber/src/test/java/org/testcontainers/examples/CucumberTest.java index 717c42351a8..5d6fe8c3f9c 100644 --- a/examples/cucumber/src/test/java/org/testcontainers/examples/CucumberTest.java +++ b/examples/cucumber/src/test/java/org/testcontainers/examples/CucumberTest.java @@ -1,9 +1,11 @@ package org.testcontainers.examples; -import io.cucumber.junit.Cucumber; -import io.cucumber.junit.CucumberOptions; -import org.junit.runner.RunWith; +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; -@RunWith(Cucumber.class) -@CucumberOptions(plugin = { "pretty" }) +@Suite +@SelectPackages("org.testcontainers.examples") +@ConfigurationParameter(key = Constants.PLUGIN_PROPERTY_NAME, value = "pretty") public class CucumberTest {} diff --git a/examples/gradle/wrapper/gradle-wrapper.jar b/examples/gradle/wrapper/gradle-wrapper.jar index d64cd491770..1b33c55baab 100644 Binary files a/examples/gradle/wrapper/gradle-wrapper.jar and b/examples/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/gradle/wrapper/gradle-wrapper.properties b/examples/gradle/wrapper/gradle-wrapper.properties index db8c3baafe3..78cb6e16a49 100644 --- a/examples/gradle/wrapper/gradle-wrapper.properties +++ b/examples/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=9d926787066a081739e8200858338b4a69e837c3a821a33aca9db09dd4a41026 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/examples/gradlew b/examples/gradlew index 1aa94a42690..23d15a93670 100755 --- a/examples/gradlew +++ b/examples/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -112,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -203,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * 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. @@ -211,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/examples/gradlew.bat b/examples/gradlew.bat index 6689b85beec..5eed7ee8452 100644 --- a/examples/gradlew.bat +++ b/examples/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,22 +59,22 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%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 diff --git a/examples/hazelcast/build.gradle b/examples/hazelcast/build.gradle index 74771e0e02b..c5086cc4d20 100644 --- a/examples/hazelcast/build.gradle +++ b/examples/hazelcast/build.gradle @@ -8,10 +8,11 @@ repositories { dependencies { testImplementation 'org.testcontainers:testcontainers' - testImplementation 'com.hazelcast:hazelcast:5.3.6' - testImplementation 'ch.qos.logback:logback-classic:1.3.14' - testImplementation 'org.assertj:assertj-core:3.25.2' - testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' + 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 { diff --git a/examples/immudb/build.gradle b/examples/immudb/build.gradle index 27d0bcea9e2..a4d656dd762 100644 --- a/examples/immudb/build.gradle +++ b/examples/immudb/build.gradle @@ -9,11 +9,12 @@ repositories { dependencies { implementation 'io.codenotary:immudb4j:1.0.1' testImplementation 'org.testcontainers:testcontainers' - testImplementation 'org.testcontainers:junit-jupiter' - testImplementation 'org.assertj:assertj-core:3.25.2' + 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.14' - testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' + 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 { diff --git a/examples/kafka-cluster/build.gradle b/examples/kafka-cluster/build.gradle index 2653c2da0b4..67ca7e4e37c 100644 --- a/examples/kafka-cluster/build.gradle +++ b/examples/kafka-cluster/build.gradle @@ -7,14 +7,16 @@ repositories { } dependencies { - testCompileOnly "org.projectlombok:lombok:1.18.30" - testAnnotationProcessor "org.projectlombok:lombok:1.18.30" - testImplementation 'org.testcontainers:kafka' - testImplementation 'org.apache.kafka:kafka-clients:3.6.1' - testImplementation 'org.assertj:assertj-core:3.25.2' + 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.14' - testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' + 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 { diff --git a/examples/kafka-cluster/src/test/java/com/example/kafkacluster/ApacheKafkaContainerCluster.java b/examples/kafka-cluster/src/test/java/com/example/kafkacluster/ApacheKafkaContainerCluster.java new file mode 100644 index 00000000000..0e56bd9d758 --- /dev/null +++ b/examples/kafka-cluster/src/test/java/com/example/kafkacluster/ApacheKafkaContainerCluster.java @@ -0,0 +1,106 @@ +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); + } +} diff --git a/examples/kafka-cluster/src/test/java/com/example/kafkacluster/ApacheKafkaContainerClusterTest.java b/examples/kafka-cluster/src/test/java/com/example/kafkacluster/ApacheKafkaContainerClusterTest.java new file mode 100644 index 00000000000..38ac274706b --- /dev/null +++ b/examples/kafka-cluster/src/test/java/com/example/kafkacluster/ApacheKafkaContainerClusterTest.java @@ -0,0 +1,94 @@ +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(); + } + } +} diff --git a/examples/kafka-cluster/src/test/java/com/example/kafkacluster/ConfluentKafkaContainerCluster.java b/examples/kafka-cluster/src/test/java/com/example/kafkacluster/ConfluentKafkaContainerCluster.java new file mode 100644 index 00000000000..f09c3072ea3 --- /dev/null +++ b/examples/kafka-cluster/src/test/java/com/example/kafkacluster/ConfluentKafkaContainerCluster.java @@ -0,0 +1,107 @@ +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); + } +} diff --git a/examples/kafka-cluster/src/test/java/com/example/kafkacluster/ConfluentKafkaContainerClusterTest.java b/examples/kafka-cluster/src/test/java/com/example/kafkacluster/ConfluentKafkaContainerClusterTest.java new file mode 100644 index 00000000000..3bb38cb7152 --- /dev/null +++ b/examples/kafka-cluster/src/test/java/com/example/kafkacluster/ConfluentKafkaContainerClusterTest.java @@ -0,0 +1,94 @@ +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(); + } + } +} diff --git a/examples/kafka-cluster/src/test/java/com/example/kafkacluster/KafkaContainerCluster.java b/examples/kafka-cluster/src/test/java/com/example/kafkacluster/KafkaContainerCluster.java index 9ab3f25e0ff..aa22cceadb0 100644 --- a/examples/kafka-cluster/src/test/java/com/example/kafkacluster/KafkaContainerCluster.java +++ b/examples/kafka-cluster/src/test/java/com/example/kafkacluster/KafkaContainerCluster.java @@ -1,7 +1,7 @@ package com.example.kafkacluster; import lombok.SneakyThrows; -import org.rnorth.ducttape.unreliables.Unreliables; +import org.awaitility.Awaitility; import org.testcontainers.containers.Container; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.KafkaContainer; @@ -11,11 +11,12 @@ import java.time.Duration; import java.util.Collection; -import java.util.concurrent.TimeUnit; 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. */ @@ -30,12 +31,14 @@ public class KafkaContainerCluster implements Startable { private final Collection brokers; public KafkaContainerCluster(String confluentPlatformVersion, int brokersNum, int internalTopicsRf) { - if (brokersNum < 0) { + if (brokersNum <= 0) { throw new IllegalArgumentException("brokersNum '" + brokersNum + "' must be greater than 0"); } - if (internalTopicsRf < 0 || internalTopicsRf > brokersNum) { + if (internalTopicsRf <= 0 || internalTopicsRf > brokersNum) { throw new IllegalArgumentException( - "internalTopicsRf '" + internalTopicsRf + "' must be less than brokersNum and greater than 0" + "internalTopicsRf '" + + internalTopicsRf + + "' must be less than or equal to brokersNum and greater than 0" ); } @@ -87,10 +90,10 @@ public void start() { // sequential start to avoid resource contention on CI systems with weaker hardware brokers.forEach(GenericContainer::start); - Unreliables.retryUntilTrue( - 30, - TimeUnit.SECONDS, - () -> { + Awaitility + .await() + .atMost(Duration.ofSeconds(30)) + .untilAsserted(() -> { Container.ExecResult result = this.zookeeper.execInContainer( "sh", @@ -101,9 +104,8 @@ public void start() { ); String brokers = result.getStdout(); - return brokers != null && brokers.split(",").length == this.brokersNum; - } - ); + assertThat(brokers.split(",")).hasSize(this.brokersNum); + }); } @Override diff --git a/examples/kafka-cluster/src/test/java/com/example/kafkacluster/KafkaContainerClusterTest.java b/examples/kafka-cluster/src/test/java/com/example/kafkacluster/KafkaContainerClusterTest.java index 51221b5d56f..6a66cf0ca2e 100644 --- a/examples/kafka-cluster/src/test/java/com/example/kafkacluster/KafkaContainerClusterTest.java +++ b/examples/kafka-cluster/src/test/java/com/example/kafkacluster/KafkaContainerClusterTest.java @@ -13,8 +13,8 @@ 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 org.rnorth.ducttape.unreliables.Unreliables; import java.time.Duration; import java.util.Collection; @@ -100,24 +100,17 @@ protected void testKafkaFunctionality(String bootstrapServers, int partitions, i producer.send(new ProducerRecord<>(topicName, "testcontainers", "rulezzz")).get(); - Unreliables.retryUntilTrue( - 10, - TimeUnit.SECONDS, - () -> { + Awaitility + .await() + .atMost(Duration.ofSeconds(10)) + .untilAsserted(() -> { ConsumerRecords records = consumer.poll(Duration.ofMillis(100)); - if (records.isEmpty()) { - return false; - } - assertThat(records) .hasSize(1) .extracting(ConsumerRecord::topic, ConsumerRecord::key, ConsumerRecord::value) .containsExactly(tuple(topicName, "testcontainers", "rulezzz")); - - return true; - } - ); + }); consumer.unsubscribe(); } diff --git a/examples/kafka-cluster/src/test/java/com/example/kafkacluster/KafkaContainerKraftCluster.java b/examples/kafka-cluster/src/test/java/com/example/kafkacluster/KafkaContainerKraftCluster.java index 4411cb14540..264f751ecc6 100644 --- a/examples/kafka-cluster/src/test/java/com/example/kafkacluster/KafkaContainerKraftCluster.java +++ b/examples/kafka-cluster/src/test/java/com/example/kafkacluster/KafkaContainerKraftCluster.java @@ -1,7 +1,7 @@ package com.example.kafkacluster; import org.apache.kafka.common.Uuid; -import org.rnorth.ducttape.unreliables.Unreliables; +import org.awaitility.Awaitility; import org.testcontainers.containers.Container; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.KafkaContainer; @@ -11,10 +11,11 @@ import java.time.Duration; import java.util.Collection; -import java.util.concurrent.TimeUnit; 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; @@ -24,12 +25,14 @@ public class KafkaContainerKraftCluster implements Startable { private final Collection brokers; public KafkaContainerKraftCluster(String confluentPlatformVersion, int brokersNum, int internalTopicsRf) { - if (brokersNum < 0) { + if (brokersNum <= 0) { throw new IllegalArgumentException("brokersNum '" + brokersNum + "' must be greater than 0"); } - if (internalTopicsRf < 0 || internalTopicsRf > brokersNum) { + if (internalTopicsRf <= 0 || internalTopicsRf > brokersNum) { throw new IllegalArgumentException( - "internalTopicsRf '" + internalTopicsRf + "' must be less than brokersNum and greater than 0" + "internalTopicsRf '" + + internalTopicsRf + + "' must be less than or equal to brokersNum and greater than 0" ); } @@ -79,10 +82,10 @@ public void start() { // Needs to start all the brokers at once brokers.parallelStream().forEach(GenericContainer::start); - Unreliables.retryUntilTrue( - 30, - TimeUnit.SECONDS, - () -> { + Awaitility + .await() + .atMost(Duration.ofSeconds(30)) + .untilAsserted(() -> { Container.ExecResult result = this.brokers.stream() .findFirst() @@ -94,13 +97,12 @@ public void start() { ); String brokers = result.getStdout().replace("\n", ""); - return brokers != null && Integer.valueOf(brokers) == this.brokersNum; - } - ); + assertThat(brokers).asInt().isEqualTo(this.brokersNum); + }); } @Override public void stop() { - this.brokers.stream().parallel().forEach(GenericContainer::stop); + this.brokers.parallelStream().forEach(GenericContainer::stop); } } diff --git a/examples/linked-container/build.gradle b/examples/linked-container/build.gradle deleted file mode 100644 index 7a3aa7156d9..00000000000 --- a/examples/linked-container/build.gradle +++ /dev/null @@ -1,17 +0,0 @@ -plugins { - id 'java' -} - -repositories { - mavenCentral() -} -dependencies { - compileOnly 'org.slf4j:slf4j-api:1.7.36' - implementation 'com.squareup.okhttp3:okhttp:4.12.0' - implementation 'org.json:json:20231013' - testRuntimeOnly 'org.postgresql:postgresql:42.7.1' - testImplementation 'ch.qos.logback:logback-classic:1.3.14' - testImplementation 'org.testcontainers:postgresql' - testImplementation 'org.assertj:assertj-core:3.25.2' -} - diff --git a/examples/linked-container/src/main/java/com/example/linkedcontainer/RedmineClient.java b/examples/linked-container/src/main/java/com/example/linkedcontainer/RedmineClient.java deleted file mode 100644 index c95ec133e2f..00000000000 --- a/examples/linked-container/src/main/java/com/example/linkedcontainer/RedmineClient.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.linkedcontainer; - -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import org.json.JSONObject; - -import java.io.IOException; - -/** - * A crude, partially implemented Redmine client. - */ -public class RedmineClient { - - private String url; - - private OkHttpClient client; - - public RedmineClient(String url) { - this.url = url; - client = new OkHttpClient(); - } - - public int getIssueCount() throws IOException { - Request request = new Request.Builder().url(url + "/issues.json").build(); - - Response response = client.newCall(request).execute(); - JSONObject jsonObject = new JSONObject(response.body().string()); - return jsonObject.getInt("total_count"); - } -} diff --git a/examples/linked-container/src/test/java/com/example/linkedcontainer/LinkedContainerTestImages.java b/examples/linked-container/src/test/java/com/example/linkedcontainer/LinkedContainerTestImages.java deleted file mode 100644 index 8bbbafd189f..00000000000 --- a/examples/linked-container/src/test/java/com/example/linkedcontainer/LinkedContainerTestImages.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.example.linkedcontainer; - -import org.testcontainers.utility.DockerImageName; - -public interface LinkedContainerTestImages { - DockerImageName POSTGRES_TEST_IMAGE = DockerImageName.parse("postgres:9.6.12"); - DockerImageName REDMINE_TEST_IMAGE = DockerImageName.parse("redmine:3.3.2"); -} diff --git a/examples/linked-container/src/test/java/com/example/linkedcontainer/RedmineClientTest.java b/examples/linked-container/src/test/java/com/example/linkedcontainer/RedmineClientTest.java deleted file mode 100644 index 721c00835c9..00000000000 --- a/examples/linked-container/src/test/java/com/example/linkedcontainer/RedmineClientTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.example.linkedcontainer; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.RuleChain; -import org.testcontainers.containers.PostgreSQLContainer; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for RedmineClient. - */ -public class RedmineClientTest { - - private static final String POSTGRES_USERNAME = "redmine"; - - private static final String POSTGRES_PASSWORD = "secret"; - - private PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer<>( - LinkedContainerTestImages.POSTGRES_TEST_IMAGE - ) - .withUsername(POSTGRES_USERNAME) - .withPassword(POSTGRES_PASSWORD); - - private RedmineContainer redmineContainer = new RedmineContainer(LinkedContainerTestImages.REDMINE_TEST_IMAGE) - .withLinkToContainer(postgreSQLContainer, "postgres") - .withEnv("POSTGRES_ENV_POSTGRES_USER", POSTGRES_USERNAME) - .withEnv("POSTGRES_ENV_POSTGRES_PASSWORD", POSTGRES_PASSWORD); - - @Rule - public RuleChain chain = RuleChain.outerRule(postgreSQLContainer).around(redmineContainer); - - @Test - public void canGetIssueCount() throws Exception { - RedmineClient redmineClient = new RedmineClient(redmineContainer.getRedmineUrl()); - - assertThat(redmineClient.getIssueCount()).as("The issue count can be retrieved.").isZero(); - } -} diff --git a/examples/linked-container/src/test/java/com/example/linkedcontainer/RedmineContainer.java b/examples/linked-container/src/test/java/com/example/linkedcontainer/RedmineContainer.java deleted file mode 100644 index 0f0bea36c1a..00000000000 --- a/examples/linked-container/src/test/java/com/example/linkedcontainer/RedmineContainer.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.example.linkedcontainer; - -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.traits.LinkableContainer; -import org.testcontainers.containers.wait.strategy.Wait; -import org.testcontainers.utility.DockerImageName; - -/** - * A Redmine container. - */ -public class RedmineContainer extends GenericContainer { - - private static final int REDMINE_PORT = 3000; - - public RedmineContainer(DockerImageName dockerImageName) { - super(dockerImageName); - } - - @Override - protected void configure() { - addExposedPort(REDMINE_PORT); - waitingFor(Wait.forHttp("/")); - } - - public RedmineContainer withLinkToContainer(LinkableContainer otherContainer, String alias) { - addLink(otherContainer, alias); - return this; - } - - public String getRedmineUrl() { - return String.format("http://%s:%d", this.getHost(), this.getMappedPort(REDMINE_PORT)); - } -} diff --git a/examples/nats/build.gradle b/examples/nats/build.gradle index e4b73de63c4..5e63a2d33a0 100644 --- a/examples/nats/build.gradle +++ b/examples/nats/build.gradle @@ -7,12 +7,13 @@ repositories { } dependencies { - testImplementation 'org.assertj:assertj-core:3.25.2' + testImplementation 'org.assertj:assertj-core:3.27.4' testImplementation 'org.testcontainers:testcontainers' - testImplementation 'io.nats:jnats:2.17.2' - testImplementation 'ch.qos.logback:logback-classic:1.3.14' + 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.10.1' + testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.11.0' } test { diff --git a/examples/neo4j-container/build.gradle b/examples/neo4j-container/build.gradle index ab4fcec66c1..7a6eed0016d 100644 --- a/examples/neo4j-container/build.gradle +++ b/examples/neo4j-container/build.gradle @@ -7,9 +7,14 @@ repositories { } dependencies { - testImplementation 'org.assertj:assertj-core:3.25.2' - testImplementation 'org.neo4j.driver:neo4j-java-driver:4.4.13' - testImplementation 'org.testcontainers:neo4j' - testImplementation 'org.testcontainers:junit-jupiter' - testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' + 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' +} + +test { + useJUnitPlatform() } diff --git a/examples/neo4j-container/src/test/java/org/testcontainers/containers/Neo4jExampleTest.java b/examples/neo4j-container/src/test/java/org/testcontainers/containers/Neo4jExampleTest.java index 38c22a0ff45..bc8770aaef0 100644 --- a/examples/neo4j-container/src/test/java/org/testcontainers/containers/Neo4jExampleTest.java +++ b/examples/neo4j-container/src/test/java/org/testcontainers/containers/Neo4jExampleTest.java @@ -22,7 +22,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; -// junitExample { @Testcontainers class Neo4jExampleTest { @@ -68,4 +67,3 @@ void testSomethingUsingHttp() throws IOException { } } } -// } diff --git a/examples/ollama-hugging-face/build.gradle b/examples/ollama-hugging-face/build.gradle new file mode 100644 index 00000000000..1d747834acc --- /dev/null +++ b/examples/ollama-hugging-face/build.gradle @@ -0,0 +1,20 @@ +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() +} diff --git a/examples/ollama-hugging-face/src/test/java/com/example/ollamahf/OllamaHuggingFaceContainer.java b/examples/ollama-hugging-face/src/test/java/com/example/ollamahf/OllamaHuggingFaceContainer.java new file mode 100644 index 00000000000..58267134a5f --- /dev/null +++ b/examples/ollama-hugging-face/src/test/java/com/example/ollamahf/OllamaHuggingFaceContainer.java @@ -0,0 +1,63 @@ +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; + } + } +} diff --git a/examples/ollama-hugging-face/src/test/java/com/example/ollamahf/OllamaHuggingFaceTest.java b/examples/ollama-hugging-face/src/test/java/com/example/ollamahf/OllamaHuggingFaceTest.java new file mode 100644 index 00000000000..fa9d5f7d7e4 --- /dev/null +++ b/examples/ollama-hugging-face/src/test/java/com/example/ollamahf/OllamaHuggingFaceTest.java @@ -0,0 +1,74 @@ +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; + } + } +} diff --git a/examples/linked-container/src/test/resources/logback-test.xml b/examples/ollama-hugging-face/src/test/resources/logback-test.xml similarity index 100% rename from examples/linked-container/src/test/resources/logback-test.xml rename to examples/ollama-hugging-face/src/test/resources/logback-test.xml diff --git a/examples/redis-backed-cache-testng/build.gradle b/examples/redis-backed-cache-testng/build.gradle index 1a3f717d506..f0d043ce978 100644 --- a/examples/redis-backed-cache-testng/build.gradle +++ b/examples/redis-backed-cache-testng/build.gradle @@ -8,13 +8,13 @@ repositories { dependencies { compileOnly 'org.slf4j:slf4j-api:1.7.36' - implementation 'redis.clients:jedis:5.1.0' - implementation 'com.google.code.gson:gson:2.10.1' + 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.14' + testImplementation 'ch.qos.logback:logback-classic:1.3.15' testImplementation 'org.testng:testng:7.5.1' - testImplementation 'org.assertj:assertj-core:3.25.2' + testImplementation 'org.assertj:assertj-core:3.27.4' } test { diff --git a/examples/redis-backed-cache-testng/src/test/java/RedisBackedCacheTest.java b/examples/redis-backed-cache-testng/src/test/java/RedisBackedCacheTest.java index 45c482c098e..0f97be3c336 100644 --- a/examples/redis-backed-cache-testng/src/test/java/RedisBackedCacheTest.java +++ b/examples/redis-backed-cache-testng/src/test/java/RedisBackedCacheTest.java @@ -17,7 +17,7 @@ */ public class RedisBackedCacheTest { - private static GenericContainer redis = new GenericContainer<>(DockerImageName.parse("redis:3.0.6")) + private static GenericContainer redis = new GenericContainer<>(DockerImageName.parse("redis:6-alpine")) .withExposedPorts(6379); private Cache cache; diff --git a/examples/redis-backed-cache/build.gradle b/examples/redis-backed-cache/build.gradle index 8ab9cac5ed9..5d20b6175dc 100644 --- a/examples/redis-backed-cache/build.gradle +++ b/examples/redis-backed-cache/build.gradle @@ -8,14 +8,15 @@ repositories { dependencies { compileOnly 'org.slf4j:slf4j-api:1.7.36' - implementation 'redis.clients:jedis:5.1.0' - implementation 'com.google.code.gson:gson:2.10.1' + 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:junit-jupiter' - testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' - testImplementation 'ch.qos.logback:logback-classic:1.3.14' - testImplementation 'org.assertj:assertj-core:3.25.2' + 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 { diff --git a/examples/redis-backed-cache/src/test/java/RedisBackedCacheTest.java b/examples/redis-backed-cache/src/test/java/RedisBackedCacheTest.java index de33245eb01..970fc0a58e5 100644 --- a/examples/redis-backed-cache/src/test/java/RedisBackedCacheTest.java +++ b/examples/redis-backed-cache/src/test/java/RedisBackedCacheTest.java @@ -19,7 +19,7 @@ class RedisBackedCacheTest { @Container - public GenericContainer redis = new GenericContainer<>(DockerImageName.parse("redis:3.0.6")) + public GenericContainer redis = new GenericContainer<>(DockerImageName.parse("redis:6-alpine")) .withExposedPorts(6379); private Cache cache; diff --git a/examples/selenium-container/build.gradle b/examples/selenium-container/build.gradle index d7b8c85510e..f1a8058e304 100644 --- a/examples/selenium-container/build.gradle +++ b/examples/selenium-container/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '2.7.18' + id 'org.springframework.boot' version '3.5.6' } apply plugin: 'io.spring.dependency-management' @@ -14,10 +14,9 @@ dependencies { 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:selenium' - testImplementation 'org.testcontainers:junit-jupiter' - testImplementation 'org.assertj:assertj-core:3.25.2' - testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' + testImplementation 'org.testcontainers:testcontainers-selenium' + testImplementation 'org.testcontainers:testcontainers-junit-jupiter' + testImplementation 'org.assertj:assertj-core:3.27.4' } test { diff --git a/examples/settings.gradle b/examples/settings.gradle index 041e9ac89c9..b9b58a917bf 100644 --- a/examples/settings.gradle +++ b/examples/settings.gradle @@ -5,13 +5,12 @@ buildscript { } } dependencies { - classpath "gradle.plugin.ch.myniva.gradle:s3-build-cache:0.10.0" - classpath "com.gradle.enterprise:com.gradle.enterprise.gradle.plugin:3.16" - classpath "com.gradle:common-custom-user-data-gradle-plugin:1.12.1" + 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.enterprise' +apply plugin: 'com.gradle.develocity' apply plugin: "com.gradle.common-custom-user-data-gradle-plugin" rootProject.name = 'testcontainers-examples' @@ -20,7 +19,6 @@ includeBuild '..' // explicit include to allow Dependabot to autodiscover subprojects include 'kafka-cluster' -include 'linked-container' include 'neo4j-container' include 'redis-backed-cache' include 'redis-backed-cache-testng' @@ -35,6 +33,7 @@ include 'zookeeper' include 'hazelcast' include 'nats' include 'sftp' +include 'ollama-hugging-face' ext.isCI = System.getenv("CI") != null @@ -42,24 +41,20 @@ buildCache { local { enabled = !isCI } - remote(HttpBuildCache) { - push = isCI && !System.getenv("READ_ONLY_REMOTE_GRADLE_CACHE") && System.getenv("GRADLE_ENTERPRISE_CACHE_PASSWORD") + remote(develocity.buildCache) { + push = isCI && !System.getenv("READ_ONLY_REMOTE_GRADLE_CACHE") && System.getenv("DEVELOCITY_ACCESS_KEY") enabled = true - url = 'https://ge.testcontainers.org/cache/' - credentials { - username = 'ci' - password = System.getenv("GRADLE_ENTERPRISE_CACHE_PASSWORD") - } } } -gradleEnterprise { +develocity { buildScan { server = "https://ge.testcontainers.org/" - publishAlways() - publishIfAuthenticated() + publishing.onlyIf { + it.authenticated + } uploadInBackground = !isCI - captureTaskInputFiles = true + capture.fileFingerprints = true } } diff --git a/examples/sftp/build.gradle b/examples/sftp/build.gradle index c37b815e6cc..d7e2dee0f45 100644 --- a/examples/sftp/build.gradle +++ b/examples/sftp/build.gradle @@ -7,11 +7,12 @@ repositories { } dependencies { - testImplementation 'com.jcraft:jsch:0.1.55' + testImplementation 'com.github.mwiede:jsch:2.27.2' testImplementation 'org.testcontainers:testcontainers' - testImplementation 'org.assertj:assertj-core:3.25.2' - testImplementation 'ch.qos.logback:logback-classic:1.3.14' - testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' + 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 { diff --git a/examples/sftp/src/test/java/org/example/SftpContainerTest.java b/examples/sftp/src/test/java/org/example/SftpContainerTest.java index e54b5b72036..3a6593ea736 100644 --- a/examples/sftp/src/test/java/org/example/SftpContainerTest.java +++ b/examples/sftp/src/test/java/org/example/SftpContainerTest.java @@ -1,6 +1,7 @@ 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; @@ -10,6 +11,7 @@ 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; @@ -49,4 +51,55 @@ void test() throws Exception { .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")); + } + } } diff --git a/examples/sftp/src/test/resources/ssh_host_rsa_key b/examples/sftp/src/test/resources/ssh_host_rsa_key new file mode 100644 index 00000000000..9987990b63d --- /dev/null +++ b/examples/sftp/src/test/resources/ssh_host_rsa_key @@ -0,0 +1,38 @@ +-----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----- diff --git a/examples/sftp/src/test/resources/ssh_host_rsa_key.pub b/examples/sftp/src/test/resources/ssh_host_rsa_key.pub new file mode 100644 index 00000000000..57b3aebb050 --- /dev/null +++ b/examples/sftp/src/test/resources/ssh_host_rsa_key.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCXMxVRzmFWxfrRB9XiZ/3HNM+xkYYE+IMGuOZD04M2ezU25XjT6cPajzpFmzTxR2qEpRCKHeVnSG5nT6UXQp7760brTN7m5sDasbMnHgYhfC/3of2k6qTR9X/JHRpgwzq5+6FtEe41w1H1dXoNIr4YTKnLijSp8MKqBtPPNUpzEVb95YKZGdCDoCbbYOyS/Dc8azUDo0mqM542J3nA2Sq9HCP0BAv43hrTAtCZodkB5wo18exbfPKsjGtA3de2npybFoSRbavZmT8L/b2iHZX6FRaqLsbYGKtszCWu5OU7WBX5g5QVlLfOnGQ+LsF6d6pX5LlMwEU14uu4gNPvZFOaZXtHNHZqnBcjd/sMaw5N/atFsPgtQ0vYnrEAD6oDjj0uXMsnmgUWTZBi3q2GBWWPqhE+0ASb2xBQGa+tWWTVYbuuYlA7hUX0URK8FcLw4UOYJjscDjnjlvQkghd2esP5NxV1NXkG2XYNHnf1E/tH4+AHJzy+qOQom7ehda96FZ8= someone@localhost diff --git a/examples/singleton-container/build.gradle b/examples/singleton-container/build.gradle index 11cb89351e9..5db92c84a49 100644 --- a/examples/singleton-container/build.gradle +++ b/examples/singleton-container/build.gradle @@ -8,15 +8,16 @@ repositories { dependencies { - implementation 'redis.clients:jedis:5.1.0' - implementation 'com.google.code.gson:gson:2.10.1' + 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.14' + testImplementation 'ch.qos.logback:logback-classic:1.3.15' testImplementation 'org.testcontainers:testcontainers' - testImplementation 'org.assertj:assertj-core:3.25.2' - testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' + 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 { diff --git a/examples/singleton-container/src/test/java/com/example/AbstractIntegrationTest.java b/examples/singleton-container/src/test/java/com/example/AbstractIntegrationTest.java index 729e9fb124d..654977d7a22 100644 --- a/examples/singleton-container/src/test/java/com/example/AbstractIntegrationTest.java +++ b/examples/singleton-container/src/test/java/com/example/AbstractIntegrationTest.java @@ -5,7 +5,7 @@ public abstract class AbstractIntegrationTest { - public static final GenericContainer redis = new GenericContainer<>(DockerImageName.parse("redis:3.0.6")) + public static final GenericContainer redis = new GenericContainer<>(DockerImageName.parse("redis:6-alpine")) .withExposedPorts(6379); static { diff --git a/examples/solr-container/build.gradle b/examples/solr-container/build.gradle index 739a418c995..d0d3944cc0a 100644 --- a/examples/solr-container/build.gradle +++ b/examples/solr-container/build.gradle @@ -7,15 +7,16 @@ repositories { } dependencies { - compileOnly "org.projectlombok:lombok:1.18.30" - annotationProcessor "org.projectlombok:lombok:1.18.30" + compileOnly "org.projectlombok:lombok:1.18.38" + annotationProcessor "org.projectlombok:lombok:1.18.38" - implementation 'org.apache.solr:solr-solrj:8.11.2' + implementation 'org.apache.solr:solr-solrj:8.11.4' testImplementation 'org.testcontainers:testcontainers' - testImplementation 'org.testcontainers:solr' - testImplementation 'org.assertj:assertj-core:3.25.2' - testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' + 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 { diff --git a/examples/spring-boot-kotlin-redis/build.gradle.kts b/examples/spring-boot-kotlin-redis/build.gradle.kts index a132efe1bba..2d5d6bf9640 100644 --- a/examples/spring-boot-kotlin-redis/build.gradle.kts +++ b/examples/spring-boot-kotlin-redis/build.gradle.kts @@ -1,9 +1,9 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - id("org.springframework.boot") version "2.7.10" - kotlin("jvm") version "1.8.10" - kotlin("plugin.spring") version "1.8.20" + 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 @@ -21,9 +21,7 @@ dependencies { testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.testcontainers:testcontainers") - testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") - - + testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.8.2") } tasks.withType { diff --git a/examples/spring-boot-kotlin-redis/src/test/kotlin/com/example/redis/AbstractIntegrationTest.kt b/examples/spring-boot-kotlin-redis/src/test/kotlin/com/example/redis/AbstractIntegrationTest.kt index 045899ad721..6df81f56262 100644 --- a/examples/spring-boot-kotlin-redis/src/test/kotlin/com/example/redis/AbstractIntegrationTest.kt +++ b/examples/spring-boot-kotlin-redis/src/test/kotlin/com/example/redis/AbstractIntegrationTest.kt @@ -16,7 +16,7 @@ import org.testcontainers.containers.GenericContainer abstract class AbstractIntegrationTest { companion object { - val redisContainer = GenericContainer("redis:3-alpine") + val redisContainer = GenericContainer("redis:6-alpine") .apply { withExposedPorts(6379) } } diff --git a/examples/spring-boot/build.gradle b/examples/spring-boot/build.gradle index 08fb733e14d..e3a40d27f06 100644 --- a/examples/spring-boot/build.gradle +++ b/examples/spring-boot/build.gradle @@ -16,8 +16,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' runtimeOnly 'org.postgresql:postgresql' testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.testcontainers:postgresql' - testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' + testImplementation 'org.testcontainers:testcontainers-postgresql' + testRuntimeOnly "org.junit.platform:junit-platform-launcher:1.8.2" } test { diff --git a/examples/spring-boot/src/test/java/com/example/AbstractIntegrationTest.java b/examples/spring-boot/src/test/java/com/example/AbstractIntegrationTest.java index d3d1621cda1..806e63ed930 100644 --- a/examples/spring-boot/src/test/java/com/example/AbstractIntegrationTest.java +++ b/examples/spring-boot/src/test/java/com/example/AbstractIntegrationTest.java @@ -16,7 +16,7 @@ @ActiveProfiles("test") abstract class AbstractIntegrationTest { - static GenericContainer redis = new GenericContainer<>(DockerImageName.parse("redis:3-alpine")) + static GenericContainer redis = new GenericContainer<>(DockerImageName.parse("redis:6-alpine")) .withExposedPorts(6379); @DynamicPropertySource diff --git a/examples/zookeeper/build.gradle b/examples/zookeeper/build.gradle index 6a5ba605b19..4208f4ac266 100644 --- a/examples/zookeeper/build.gradle +++ b/examples/zookeeper/build.gradle @@ -7,11 +7,12 @@ repositories { } dependencies { - testImplementation 'org.apache.curator:curator-framework:5.6.0' + testImplementation 'org.apache.curator:curator-framework:5.9.0' testImplementation 'org.testcontainers:testcontainers' - testImplementation 'org.assertj:assertj-core:3.25.2' - testImplementation 'ch.qos.logback:logback-classic:1.3.14' - testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' + 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 { diff --git a/gradle.properties b/gradle.properties index 4fb587917e9..f9e383482f4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,6 @@ org.gradle.parallel=false org.gradle.caching=true org.gradle.configureondemand=true -testcontainers.version=1.19.4 +org.gradle.jvmargs=-Xmx2g + +testcontainers.version=2.0.5 diff --git a/gradle/publishing.gradle b/gradle/publishing.gradle index 618ada345cc..3e3faaf08ce 100644 --- a/gradle/publishing.gradle +++ b/gradle/publishing.gradle @@ -1,4 +1,5 @@ apply plugin: 'maven-publish' +apply plugin: 'org.jreleaser' task sourceJar(type: Jar) { archiveClassifier.set( 'sources') @@ -95,10 +96,27 @@ publishing { } repositories { maven { - url("https://oss.sonatype.org/service/local/staging/deploy/maven2") - credentials { - username = System.getenv("OSSRH_USERNAME") - password = System.getenv("OSSRH_PASSWORD") + 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' + } } } } diff --git a/gradle/shading.gradle b/gradle/shading.gradle index fc7f6587b3e..88f25cc3f22 100644 --- a/gradle/shading.gradle +++ b/gradle/shading.gradle @@ -1,10 +1,10 @@ import java.util.jar.JarFile -apply plugin: 'com.github.johnrengelman.shadow' +apply plugin: 'com.gradleup.shadow' configurations { shaded - [apiElements, implementation, compileOnly, testCompile]*.extendsFrom shaded + [apiElements, implementation]*.extendsFrom shaded } configurations.api.canBeResolved = true @@ -30,7 +30,7 @@ project.afterEvaluate { return it.dependencyProject.tasks.findByName("shadowJar")?.relocators ?: [] } - // See https://github.com/johnrengelman/shadow/blob/5.0.0/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ConfigureShadowRelocation.groovy + // See https://github.com/GradleUp/shadow/blob/5.0.0/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ConfigureShadowRelocation.groovy Set packages = [] for (artifact in project.configurations.shaded.resolvedConfiguration.resolvedArtifacts) { diff --git a/gradle/spotless.gradle b/gradle/spotless.gradle index ce71829331e..59b834dbad7 100644 --- a/gradle/spotless.gradle +++ b/gradle/spotless.gradle @@ -18,6 +18,6 @@ spotless { } groovyGradle { target '**/*.groovy' - greclipse('4.19.0') + greclipse('4.19') } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd491770..1b33c55baab 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a7a990ab2a8..dbc089ed3d7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=c16d517b50dd28b3f5838f0e844b7520b8f1eb610f2f29de7e4e04a1b7c9c79b -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip +distributionSha256Sum=ed1a8d686605fd7c23bdf62c7fc7add1c5b23b2bbc3721e661934ef4a4911d7c +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a42690..23d15a93670 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -112,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -203,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * 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. @@ -211,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 6689b85beec..5eed7ee8452 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,22 +59,22 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%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 diff --git a/mkdocs.yml b/mkdocs.yml index de6030812e6..ae447212912 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -56,43 +56,59 @@ nav: - modules/databases/couchbase.md - modules/databases/clickhouse.md - modules/databases/cratedb.md + - modules/databases/databend.md - modules/databases/db2.md - - modules/databases/dynalite.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 @@ -121,7 +137,6 @@ nav: - Contributing: - contributing.md - contributing_docs.md - - bounty.md edit_uri: edit/main/docs/ extra: - latest_version: 1.19.4 + latest_version: 2.0.5 diff --git a/modules/activemq/build.gradle b/modules/activemq/build.gradle index 66bf306fd49..c77b386698f 100644 --- a/modules/activemq/build.gradle +++ b/modules/activemq/build.gradle @@ -3,20 +3,6 @@ description = "Testcontainers :: ActiveMQ" dependencies { api project(':testcontainers') - testImplementation 'org.assertj:assertj-core:3.25.1' - testImplementation "org.apache.activemq:activemq-client:6.0.1" - testImplementation "org.apache.activemq:artemis-jakarta-client:2.31.2" -} - -test { - javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(17) - } -} - -compileTestJava { - javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(17) - } - options.release.set(11) + testImplementation "org.apache.activemq:activemq-client:6.2.4" + testImplementation "org.apache.activemq:artemis-jakarta-client:2.55.0" } diff --git a/modules/activemq/src/main/java/org/testcontainers/activemq/ActiveMQContainer.java b/modules/activemq/src/main/java/org/testcontainers/activemq/ActiveMQContainer.java index 14814d95da9..9313efc55d6 100644 --- a/modules/activemq/src/main/java/org/testcontainers/activemq/ActiveMQContainer.java +++ b/modules/activemq/src/main/java/org/testcontainers/activemq/ActiveMQContainer.java @@ -21,7 +21,11 @@ */ public class ActiveMQContainer extends GenericContainer { - private static final DockerImageName DEFAULT_IMAGE = DockerImageName.parse("apache/activemq-classic"); + 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; @@ -45,7 +49,7 @@ public ActiveMQContainer(String image) { public ActiveMQContainer(DockerImageName dockerImageName) { super(dockerImageName); - dockerImageName.assertCompatibleWith(DEFAULT_IMAGE); + 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))); diff --git a/modules/activemq/src/main/java/org/testcontainers/activemq/ArtemisContainer.java b/modules/activemq/src/main/java/org/testcontainers/activemq/ArtemisContainer.java index 7d4918a9306..82072993b48 100644 --- a/modules/activemq/src/main/java/org/testcontainers/activemq/ArtemisContainer.java +++ b/modules/activemq/src/main/java/org/testcontainers/activemq/ArtemisContainer.java @@ -9,6 +9,8 @@ /** * Testcontainers implementation for Apache ActiveMQ Artemis. *

+ * Supported images: {@code apache/artemis}, {@code apache/activemq-artemis} + *

* Exposed ports: *

    *
  • Console: 8161
  • @@ -24,6 +26,8 @@ public class ArtemisContainer extends GenericContainer { private static final DockerImageName DEFAULT_IMAGE = DockerImageName.parse("apache/activemq-artemis"); + private static final DockerImageName APACHE_ARTEMIS_IMAGE = DockerImageName.parse("apache/artemis"); + private static final int WEB_CONSOLE_PORT = 8161; // CORE,MQTT,AMQP,HORNETQ,STOMP,OPENWIRE @@ -49,7 +53,7 @@ public ArtemisContainer(String image) { public ArtemisContainer(DockerImageName dockerImageName) { super(dockerImageName); - dockerImageName.assertCompatibleWith(DEFAULT_IMAGE); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE, APACHE_ARTEMIS_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))); diff --git a/modules/activemq/src/test/java/org/testcontainers/activemq/ActiveMQContainerTest.java b/modules/activemq/src/test/java/org/testcontainers/activemq/ActiveMQContainerTest.java index 3ccd7acf2b1..cb66ebba864 100644 --- a/modules/activemq/src/test/java/org/testcontainers/activemq/ActiveMQContainerTest.java +++ b/modules/activemq/src/test/java/org/testcontainers/activemq/ActiveMQContainerTest.java @@ -3,23 +3,24 @@ import jakarta.jms.Connection; import jakarta.jms.ConnectionFactory; import jakarta.jms.Destination; -import jakarta.jms.JMSException; import jakarta.jms.MessageConsumer; import jakarta.jms.MessageProducer; import jakarta.jms.Session; import jakarta.jms.TextMessage; import lombok.SneakyThrows; import org.apache.activemq.ActiveMQConnectionFactory; -import org.junit.Test; +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; -public class ActiveMQContainerTest { +class ActiveMQContainerTest { @Test - public void test() throws JMSException { + void test() { try ( // container { - ActiveMQContainer activemq = new ActiveMQContainer("apache/activemq-classic:5.18.3") + ActiveMQContainer activemq = new ActiveMQContainer("apache/activemq:5.18.7") // } ) { activemq.start(); @@ -30,11 +31,20 @@ public void test() throws JMSException { } } + @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 - public void customCredentials() { + void customCredentials() { try ( // settingCredentials { - ActiveMQContainer activemq = new ActiveMQContainer("apache/activemq-classic:5.18.3") + ActiveMQContainer activemq = new ActiveMQContainer("apache/activemq:5.18.7") .withUser("testcontainers") .withPassword("testcontainers") // } diff --git a/modules/activemq/src/test/java/org/testcontainers/activemq/ArtemisContainerTest.java b/modules/activemq/src/test/java/org/testcontainers/activemq/ArtemisContainerTest.java index cce4b319afd..74cdfa76da7 100644 --- a/modules/activemq/src/test/java/org/testcontainers/activemq/ArtemisContainerTest.java +++ b/modules/activemq/src/test/java/org/testcontainers/activemq/ArtemisContainerTest.java @@ -8,17 +8,19 @@ import jakarta.jms.TextMessage; import lombok.SneakyThrows; import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory; -import org.junit.Test; +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; -public class ArtemisContainerTest { +class ArtemisContainerTest { @Test - public void defaultCredentials() { + void defaultCredentials() { try ( // container { - ArtemisContainer artemis = new ArtemisContainer("apache/activemq-artemis:2.30.0-alpine") + ArtemisContainer artemis = new ArtemisContainer("apache/activemq-artemis:2.32.0-alpine") // } ) { artemis.start(); @@ -30,10 +32,10 @@ public void defaultCredentials() { } @Test - public void customCredentials() { + void customCredentials() { try ( // settingCredentials { - ArtemisContainer artemis = new ArtemisContainer("apache/activemq-artemis:2.30.0-alpine") + ArtemisContainer artemis = new ArtemisContainer("apache/activemq-artemis:2.32.0-alpine") .withUser("testcontainers") .withPassword("testcontainers") // } @@ -47,10 +49,10 @@ public void customCredentials() { } @Test - public void allowAnonymousLogin() { + void allowAnonymousLogin() { try ( // enableAnonymousLogin { - ArtemisContainer artemis = new ArtemisContainer("apache/activemq-artemis:2.30.0-alpine") + ArtemisContainer artemis = new ArtemisContainer("apache/activemq-artemis:2.32.0-alpine") .withEnv("ANONYMOUS_LOGIN", "true") // } ) { @@ -60,6 +62,15 @@ public void allowAnonymousLogin() { } } + @ParameterizedTest + @ValueSource(strings = { "apache/activemq-artemis:2.32.0-alpine", "apache/artemis:2.53.0-alpine" }) + void compatibility(String image) { + try (ArtemisContainer artemis = new ArtemisContainer(image)) { + artemis.start(); + assertFunctionality(artemis, false); + } + } + @SneakyThrows private void assertFunctionality(ArtemisContainer artemis, boolean anonymousLogin) { ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(artemis.getBrokerUrl()); diff --git a/modules/azure/build.gradle b/modules/azure/build.gradle index 7a3da191f05..167c0c26f1a 100644 --- a/modules/azure/build.gradle +++ b/modules/azure/build.gradle @@ -2,9 +2,20 @@ 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:4.12.0' + shaded 'com.squareup.okhttp3:okhttp:5.4.0' - testImplementation 'org.assertj:assertj-core:3.25.1' - testImplementation 'com.azure:azure-cosmos:4.54.0' + 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.4.0.jre11' +} + +tasks.japicmp { + methodExcludes = ["org.testcontainers.azure.ServiceBusEmulatorContainer#withMsSqlServerContainer(org.testcontainers.containers.MSSQLServerContainer)"] } diff --git a/modules/azure/src/main/java/org/testcontainers/azure/AzuriteContainer.java b/modules/azure/src/main/java/org/testcontainers/azure/AzuriteContainer.java new file mode 100644 index 00000000000..11e34237740 --- /dev/null +++ b/modules/azure/src/main/java/org/testcontainers/azure/AzuriteContainer.java @@ -0,0 +1,190 @@ +package org.testcontainers.azure; + +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.List; + +/** + * 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; + + private final List commandOptions = new ArrayList<>(); + + /** + * @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; + } + + /** + * Append extra Azurite command line flags after the default host and SSL arguments. + *

    + * {@link #withCommand(String...)} cannot be used for this: {@link #configure()} always + * rebuilds the process command and overwrites any previously set command. + * + * @param options additional Azurite flags, for example {@code --skipApiVersionCheck} + * @return this + */ + public AzuriteContainer withCommandOptions(String... options) { + this.commandOptions.addAll(Arrays.asList(options)); + 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"); + } + } + for (String option : this.commandOptions) { + args.append(" ").append(option); + } + final String cmd = args.toString(); + logger().debug("Using command line: '{}'", cmd); + return cmd; + } +} diff --git a/modules/azure/src/main/java/org/testcontainers/azure/EventHubsEmulatorContainer.java b/modules/azure/src/main/java/org/testcontainers/azure/EventHubsEmulatorContainer.java new file mode 100644 index 00000000000..257f71a1424 --- /dev/null +++ b/modules/azure/src/main/java/org/testcontainers/azure/EventHubsEmulatorContainer.java @@ -0,0 +1,109 @@ +package org.testcontainers.azure; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.builder.Transferable; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.LicenseAcceptance; + +/** + * Testcontainers implementation for Azure Eventhubs Emulator. + *

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

    + * Exposed ports: + *

      + *
    • 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)); + } +} diff --git a/modules/azure/src/main/java/org/testcontainers/azure/ServiceBusEmulatorContainer.java b/modules/azure/src/main/java/org/testcontainers/azure/ServiceBusEmulatorContainer.java new file mode 100644 index 00000000000..4557b6acb6a --- /dev/null +++ b/modules/azure/src/main/java/org/testcontainers/azure/ServiceBusEmulatorContainer.java @@ -0,0 +1,107 @@ +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)); + } +} diff --git a/modules/azure/src/main/java/org/testcontainers/containers/CosmosDBEmulatorContainer.java b/modules/azure/src/main/java/org/testcontainers/containers/CosmosDBEmulatorContainer.java index f5238a45d33..a1cfdeaf124 100644 --- a/modules/azure/src/main/java/org/testcontainers/containers/CosmosDBEmulatorContainer.java +++ b/modules/azure/src/main/java/org/testcontainers/containers/CosmosDBEmulatorContainer.java @@ -27,7 +27,7 @@ public CosmosDBEmulatorContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); withExposedPorts(PORT); - waitingFor(Wait.forLogMessage("(?s).*Started\\r\\n$", 1)); + waitingFor(Wait.forLogMessage(".*Started\\r\\n$", 1)); } /** diff --git a/modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerCommandTest.java b/modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerCommandTest.java new file mode 100644 index 00000000000..c12e77ed69a --- /dev/null +++ b/modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerCommandTest.java @@ -0,0 +1,62 @@ +package org.testcontainers.azure; + +import org.junit.jupiter.api.Test; +import org.testcontainers.utility.MountableFile; + +import static org.assertj.core.api.Assertions.assertThat; + +class AzuriteContainerCommandTest { + + private static final String IMAGE = "mcr.microsoft.com/azure-storage/azurite:3.33.0"; + + @Test + void commandLineOmitsExtraOptionsByDefault() { + AzuriteContainer emulator = new AzuriteContainer(IMAGE); + + assertThat(emulator.getCommandLine()).doesNotContain("--skipApiVersionCheck"); + } + + @Test + void commandLineIncludesExtraOptions() { + // commandOptions { + AzuriteContainer emulator = new AzuriteContainer("mcr.microsoft.com/azure-storage/azurite:3.33.0") + .withCommandOptions("--skipApiVersionCheck"); + // } + + assertThat(emulator.getCommandLine()) + .startsWith("azurite --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0") + .endsWith("--skipApiVersionCheck"); + } + + @Test + void commandLineAppendsMultipleOptions() { + AzuriteContainer emulator = new AzuriteContainer(IMAGE) + .withCommandOptions("--skipApiVersionCheck", "--disableProductStyleUrl"); + + assertThat(emulator.getCommandLine()).endsWith("--skipApiVersionCheck --disableProductStyleUrl"); + } + + @Test + void commandLineKeepsExtraOptionsTogetherWithSsl() { + AzuriteContainer emulator = new AzuriteContainer(IMAGE) + .withSsl(MountableFile.forClasspathResource("/keystore.pfx"), "changeit") + .withCommandOptions("--skipApiVersionCheck"); + + assertThat(emulator.getCommandLine()) + .contains("--cert /cert.pfx") + .endsWith("--pwd changeit --skipApiVersionCheck"); + } + + @Test + void configureAppliesCommandOptionsEvenIfWithCommandWasUsed() { + AzuriteContainer emulator = new AzuriteContainer(IMAGE) + .withCommand("azurite --ignored") + .withCommandOptions("--skipApiVersionCheck"); + + emulator.configure(); + + assertThat(String.join(" ", emulator.getCommandParts())) + .isEqualTo("azurite --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0 --skipApiVersionCheck"); + assertThat(emulator.getCommandLine()).contains("--blobHost 0.0.0.0").contains("--skipApiVersionCheck"); + } +} diff --git a/modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerTest.java b/modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerTest.java new file mode 100644 index 00000000000..5acf2fe5ad5 --- /dev/null +++ b/modules/azure/src/test/java/org/testcontainers/azure/AzuriteContainerTest.java @@ -0,0 +1,266 @@ +package org.testcontainers.azure; + +import com.azure.core.util.BinaryData; +import com.azure.data.tables.TableClient; +import com.azure.data.tables.TableServiceClient; +import com.azure.data.tables.TableServiceClientBuilder; +import com.azure.storage.blob.BlobClient; +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.BlobServiceClientBuilder; +import com.azure.storage.queue.QueueClient; +import com.azure.storage.queue.QueueServiceClient; +import com.azure.storage.queue.QueueServiceClientBuilder; +import org.junit.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(); + } +} diff --git a/modules/azure/src/test/java/org/testcontainers/azure/EventHubsEmulatorContainerTest.java b/modules/azure/src/test/java/org/testcontainers/azure/EventHubsEmulatorContainerTest.java new file mode 100644 index 00000000000..1191c980edd --- /dev/null +++ b/modules/azure/src/test/java/org/testcontainers/azure/EventHubsEmulatorContainerTest.java @@ -0,0 +1,74 @@ +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"); + }); + } + } +} diff --git a/modules/azure/src/test/java/org/testcontainers/azure/ServiceBusEmulatorContainerTest.java b/modules/azure/src/test/java/org/testcontainers/azure/ServiceBusEmulatorContainerTest.java new file mode 100644 index 00000000000..83cd633c5a9 --- /dev/null +++ b/modules/azure/src/test/java/org/testcontainers/azure/ServiceBusEmulatorContainerTest.java @@ -0,0 +1,99 @@ +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(); + } + } +} diff --git a/modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java b/modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java index 140bd64c39a..92b5814641e 100644 --- a/modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java +++ b/modules/azure/src/test/java/org/testcontainers/containers/CosmosDBEmulatorContainerTest.java @@ -4,11 +4,10 @@ import com.azure.cosmos.CosmosClientBuilder; import com.azure.cosmos.models.CosmosContainerResponse; import com.azure.cosmos.models.CosmosDatabaseResponse; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; +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; @@ -18,59 +17,60 @@ import static org.assertj.core.api.Assertions.assertThat; -public class CosmosDBEmulatorContainerTest { +class CosmosDBEmulatorContainerTest { private static Properties originalSystemProperties; - @BeforeClass + @BeforeAll public static void captureOriginalSystemProperties() { originalSystemProperties = (Properties) System.getProperties().clone(); } - @AfterClass + @AfterAll public static void restoreOriginalSystemProperties() { System.setProperties(originalSystemProperties); } - @Rule - public TemporaryFolder tempFolder = TemporaryFolder.builder().assureDeletion().build(); - - @Rule - // emulatorContainer { - public CosmosDBEmulatorContainer emulator = new CosmosDBEmulatorContainer( - DockerImageName.parse("mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:latest") - ); - - // } + @TempDir + public Path tempFolder; @Test - public void testWithCosmosClient() throws Exception { - // buildAndSaveNewKeyStore { - Path keyStoreFile = tempFolder.newFile("azure-cosmos-emulator.keystore").toPath(); - 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); - // } + 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); + // } + } } } diff --git a/modules/azure/src/test/resources/certificate.pem b/modules/azure/src/test/resources/certificate.pem new file mode 100644 index 00000000000..30bedc29f45 --- /dev/null +++ b/modules/azure/src/test/resources/certificate.pem @@ -0,0 +1,23 @@ +Bag Attributes + friendlyName: localhost + localKeyID: 54 69 6D 65 20 31 37 33 34 37 32 32 33 32 31 33 31 39 +subject=CN = localhost +issuer=CN = localhost +-----BEGIN CERTIFICATE----- +MIIC5zCCAc+gAwIBAgIILe7i2bhRE5cwDQYJKoZIhvcNAQEMBQAwFDESMBAGA1UE +AxMJbG9jYWxob3N0MB4XDTI0MTIyMDE5MTg0MVoXDTQ0MTIxNTE5MTg0MVowFDES +MBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAoqYNmLl8IiIoYrXdcoWiMQaM0lOHcV9v3A/THMremHxsR+JPm3FIOAuilFcy +my16kuXIWHfisPxUWr9Vbf8wP/WwZutoOofJrqmruZoorQcNLCs8mQweguRmL1ju +/lDh/9bP626vP9OjwStC4UO4f8Jga8ENoH1U+j1RsIAswYnkk3YIN6YrYv66UvtH +IfR0ERgid2LMBIM+2KD2zw4QRyqXH7Qvo7sCsxdYYHGa6GXfza4vgvce9kJwGqn5 +wiF0Uw9XQbr/LarnR2GCy020OB81KweQJNIh27FZSRLtT+XpsjDRcC2aLBd8CRHd +hwO2zAPI04dLbLM5XAHlEdfT7wIDAQABoz0wOzAdBgNVHQ4EFgQUPqY5isb6Q11Q +t6dbXYHEupxADdMwGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3 +DQEBDAUAA4IBAQA2katMXrTJBukiNh9yceLO/MewsxvU3KOO/O89ngfjhKXm9T8E +RtENCmp7hLbj1Aj4PRZx3AbmUt9+tRu8fmrRXJQWgUDSHJWjDwSTBOaHcC5LDWSU +Ex4co5Mnxvrimg7tqQg82Hw/yLH9j6gyTyh6v45QETP7IUkTZe4fg75/kPjng7Xg +wp/QXFUx/f0dbvGRl2Fdgg0SnYFqHS3MFIjjFjv8SQlV7rZe+CD1Lxqy/Z6Fd/Fa +33TzTuJeSAG43vdkGAvsNK/KdnxAW03T4l3pVHpNPcvsIvJUMeKOwYOjwHF/eowk +tGrKbpUYFxUr9iKHTfu14t1oExhAsnda2Fcs +-----END CERTIFICATE----- diff --git a/modules/azure/src/test/resources/eventhubs_config.json b/modules/azure/src/test/resources/eventhubs_config.json new file mode 100644 index 00000000000..554be9d7cbf --- /dev/null +++ b/modules/azure/src/test/resources/eventhubs_config.json @@ -0,0 +1,24 @@ +{ + "UserConfig": { + "NamespaceConfig": [ + { + "Type": "EventHub", + "Name": "emulatorNs1", + "Entities": [ + { + "Name": "eh1", + "PartitionCount": "1", + "ConsumerGroups": [ + { + "Name": "cg1" + } + ] + } + ] + } + ], + "LoggingConfig": { + "Type": "File" + } + } +} diff --git a/modules/azure/src/test/resources/key.pem b/modules/azure/src/test/resources/key.pem new file mode 100644 index 00000000000..7c635f5a278 --- /dev/null +++ b/modules/azure/src/test/resources/key.pem @@ -0,0 +1,32 @@ +Bag Attributes + friendlyName: localhost + localKeyID: 54 69 6D 65 20 31 37 33 34 37 32 32 33 32 31 33 31 39 +Key Attributes: +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCipg2YuXwiIihi +td1yhaIxBozSU4dxX2/cD9Mcyt6YfGxH4k+bcUg4C6KUVzKbLXqS5chYd+Kw/FRa +v1Vt/zA/9bBm62g6h8muqau5miitBw0sKzyZDB6C5GYvWO7+UOH/1s/rbq8/06PB +K0LhQ7h/wmBrwQ2gfVT6PVGwgCzBieSTdgg3piti/rpS+0ch9HQRGCJ3YswEgz7Y +oPbPDhBHKpcftC+juwKzF1hgcZroZd/Nri+C9x72QnAaqfnCIXRTD1dBuv8tqudH +YYLLTbQ4HzUrB5Ak0iHbsVlJEu1P5emyMNFwLZosF3wJEd2HA7bMA8jTh0tsszlc +AeUR19PvAgMBAAECggEAFT8dzZKFTawqnGJncBtWyZKyeJMiwUOXSCblDADQPRkb +x/QfNA4DQhb7AOe3G6BAP8o2dqAKg9YiasxNq5XHRsOgbIFZ1zN/vAo7/X3OzHN8 +XAW138Q+hBiz5IF4js4gB5yXAokt6WeLH6O4E9cV1dKdZ9YLIqjcnee+sRC9R/a3 +CexqLfC6b77JFbtePfq+5cn2RiK540tO/4k+F+kfJtTg78Wf2RB3A0pBAunhPSd5 +eyjiSvOZtTcvl4GdYw9nKf24I1/WUvt9FH/r1XG0CM5iwuGodbBz1iSUDaQGi5Lf +hFWofXt7eebgsPEKciG4xTyk51p9fy9y8asY+jCbkQKBgQDG2UqJToR8G4Fk6uaO +/XJa15TibIwQDEota0OdlXg2ZR4864fkIBv+UTbymZEM/EBuSdMM1CUBDvYHcFQX +Aj8p1LUyKP2QYwxV/OoPfJ5fBqxqONNR1fLFg7xCxnf9kSvsni2WFneQUrTDl8+7 +qnHm4IKPkAxZ4Orxl5qIBmlpGQKBgQDRZUL3cHIVLLg/aZACpo6SYDYg2bztXmz4 +lRk9j17q1uS83Umzd2lPFmSt/Nr85EKraxXZ/lYPKrP/r1pf1/35eXOWqmYBWgo/ +Hh7OzL12bhvv9UWEY/TvW+wNJNtXlJSjEFRN4tjoG2amYumyhwMO1lIulplUWvtw +ymm8hDjeRwKBgCq7n60KVqZlMtWBNbMc/GpRUgmm0iLQwVApcQp4iLEH4gutgjKg +Q+PPiENyhR2JSD9rVhO3s4warvzCQw/+x5wxvg7diEBzSL9h7tsNKOu6/2qEc8Vu +eRHBUb/37ulrPUlIZPuQMHmvjHFMOrRV2MyJCwXXKxBVqafpsKfy2MxhAoGBAIHH +Cswk6u/ouYDDwjeCVxatfp65lHhhb5RZhD09IIzYBwhu9gC+34veyyNydZ8LMa7g +PbjQAzJ/OvQbEB4a1hPKjDMzBOmNjpAz8NAm4L4H3FTKZP16nhHDnPdAgpkzQzQV +KMrk755bbTFuWH0HZIPLnT+2ou0/PltXeFUYdc59AoGBAIGfWgSOiw7aXbSQZFrO +4S0v3VTwTaiGDVS4pkNRLlhEJUhy8+gbLv/zYDmFmGtqVhXTb/nd6DOdylp+W/HS +8xNWBMWdlX/hVdSK7M0TdJvAaCaMidlquf5qZ2tGNNDeTUN1qbRH26pm8vdNZ3gr +Y/WWJGo0iEmwyB8RcFhvNmuJ +-----END PRIVATE KEY----- diff --git a/modules/azure/src/test/resources/keystore.pfx b/modules/azure/src/test/resources/keystore.pfx new file mode 100644 index 00000000000..3fd2975d3e8 Binary files /dev/null and b/modules/azure/src/test/resources/keystore.pfx differ diff --git a/modules/azure/src/test/resources/service-bus-config.json b/modules/azure/src/test/resources/service-bus-config.json new file mode 100644 index 00000000000..18ac2e69c7b --- /dev/null +++ b/modules/azure/src/test/resources/service-bus-config.json @@ -0,0 +1,29 @@ +{ + "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" + } + } +} diff --git a/modules/cassandra/build.gradle b/modules/cassandra/build.gradle index 79492c044db..b0bea00226e 100644 --- a/modules/cassandra/build.gradle +++ b/modules/cassandra/build.gradle @@ -7,9 +7,8 @@ configurations.all { } dependencies { - api project(":database-commons") + 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' - testImplementation 'org.assertj:assertj-core:3.25.1' } diff --git a/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraContainer.java b/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraContainer.java new file mode 100644 index 00000000000..3fb8554f137 --- /dev/null +++ b/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraContainer.java @@ -0,0 +1,211 @@ +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); + } +} diff --git a/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraDatabaseDelegate.java b/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraDatabaseDelegate.java new file mode 100644 index 00000000000..4867b8423e9 --- /dev/null +++ b/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraDatabaseDelegate.java @@ -0,0 +1,98 @@ +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. + } +} diff --git a/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraQueryWaitStrategy.java b/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraQueryWaitStrategy.java new file mode 100644 index 00000000000..19fdcd7f9e1 --- /dev/null +++ b/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraQueryWaitStrategy.java @@ -0,0 +1,57 @@ +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); + } +} diff --git a/modules/cassandra/src/main/java/org/testcontainers/containers/CassandraContainer.java b/modules/cassandra/src/main/java/org/testcontainers/containers/CassandraContainer.java index bb291cd1f80..fcaada177ce 100644 --- a/modules/cassandra/src/main/java/org/testcontainers/containers/CassandraContainer.java +++ b/modules/cassandra/src/main/java/org/testcontainers/containers/CassandraContainer.java @@ -24,7 +24,10 @@ * 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"); @@ -225,7 +228,7 @@ public static Cluster getCluster(ContainerState containerState) { /** * Retrieve an {@link InetSocketAddress} for connecting to the Cassandra container via the driver. * - * @return A InetSocketAddrss representation of this Cassandra container's host and port. + * @return A InetSocketAddress representation of this Cassandra container's host and port. */ public InetSocketAddress getContactPoint() { return new InetSocketAddress(getHost(), getMappedPort(CQL_PORT)); diff --git a/modules/cassandra/src/main/java/org/testcontainers/containers/delegate/CassandraDatabaseDelegate.java b/modules/cassandra/src/main/java/org/testcontainers/containers/delegate/CassandraDatabaseDelegate.java index 8d604822083..7b7739490ea 100644 --- a/modules/cassandra/src/main/java/org/testcontainers/containers/delegate/CassandraDatabaseDelegate.java +++ b/modules/cassandra/src/main/java/org/testcontainers/containers/delegate/CassandraDatabaseDelegate.java @@ -13,9 +13,12 @@ /** * Cassandra database delegate + * + * @deprecated use {@link org.testcontainers.cassandra.CassandraDatabaseDelegate} instead. */ @Slf4j @RequiredArgsConstructor +@Deprecated public class CassandraDatabaseDelegate extends AbstractDatabaseDelegate { private final ContainerState container; diff --git a/modules/cassandra/src/main/java/org/testcontainers/containers/wait/CassandraQueryWaitStrategy.java b/modules/cassandra/src/main/java/org/testcontainers/containers/wait/CassandraQueryWaitStrategy.java index cffb1ba757e..9694711de6e 100644 --- a/modules/cassandra/src/main/java/org/testcontainers/containers/wait/CassandraQueryWaitStrategy.java +++ b/modules/cassandra/src/main/java/org/testcontainers/containers/wait/CassandraQueryWaitStrategy.java @@ -12,7 +12,10 @@ /** * 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"; diff --git a/modules/cassandra/src/main/resources/cqlshrc b/modules/cassandra/src/main/resources/cqlshrc new file mode 100644 index 00000000000..02e001098af --- /dev/null +++ b/modules/cassandra/src/main/resources/cqlshrc @@ -0,0 +1,7 @@ +[ssl] +certfile = ssl/user_cert.pem +usercert = ssl/user_cert.pem +userkey = ssl/user_key.pem + +[connection] +factory = cqlshlib.ssl.ssl_transport_factory \ No newline at end of file diff --git a/modules/cassandra/src/test/java/org/testcontainers/cassandra/CassandraContainerTest.java b/modules/cassandra/src/test/java/org/testcontainers/cassandra/CassandraContainerTest.java new file mode 100644 index 00000000000..49243317a40 --- /dev/null +++ b/modules/cassandra/src/test/java/org/testcontainers/cassandra/CassandraContainerTest.java @@ -0,0 +1,272 @@ +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; + } +} diff --git a/modules/cassandra/src/test/java/org/testcontainers/cassandra/CompatibleCassandraImageTest.java b/modules/cassandra/src/test/java/org/testcontainers/cassandra/CompatibleCassandraImageTest.java new file mode 100644 index 00000000000..15d8e209911 --- /dev/null +++ b/modules/cassandra/src/test/java/org/testcontainers/cassandra/CompatibleCassandraImageTest.java @@ -0,0 +1,44 @@ +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(); + } + } +} diff --git a/modules/cassandra/src/test/java/org/testcontainers/containers/CassandraContainerTest.java b/modules/cassandra/src/test/java/org/testcontainers/containers/CassandraContainerTest.java index eb3a8b068ea..2d534bccc81 100644 --- a/modules/cassandra/src/test/java/org/testcontainers/containers/CassandraContainerTest.java +++ b/modules/cassandra/src/test/java/org/testcontainers/containers/CassandraContainerTest.java @@ -5,14 +5,15 @@ import com.datastax.driver.core.Row; import com.datastax.driver.core.Session; import lombok.extern.slf4j.Slf4j; -import org.junit.Test; +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 -public class CassandraContainerTest { +class CassandraContainerTest { private static final DockerImageName CASSANDRA_IMAGE = DockerImageName.parse("cassandra:3.11.2"); @@ -21,7 +22,7 @@ public class CassandraContainerTest { private static final String BASIC_QUERY = "SELECT release_version FROM system.local"; @Test - public void testSimple() { + void testSimple() { try (CassandraContainer cassandraContainer = new CassandraContainer<>(CASSANDRA_IMAGE)) { cassandraContainer.start(); ResultSet resultSet = performQuery(cassandraContainer, BASIC_QUERY); @@ -31,7 +32,7 @@ public void testSimple() { } @Test - public void testSpecificVersion() { + void testSpecificVersion() { String cassandraVersion = "3.0.15"; try ( CassandraContainer cassandraContainer = new CassandraContainer<>( @@ -46,7 +47,7 @@ public void testSpecificVersion() { } @Test - public void testConfigurationOverride() { + void testConfigurationOverride() { try ( CassandraContainer cassandraContainer = new CassandraContainer<>(CASSANDRA_IMAGE) .withConfigurationOverride("cassandra-test-configuration-example") @@ -60,18 +61,18 @@ public void testConfigurationOverride() { } } - @Test(expected = ContainerLaunchException.class) - public void testEmptyConfigurationOverride() { + @Test + void testEmptyConfigurationOverride() { try ( CassandraContainer cassandraContainer = new CassandraContainer<>(CASSANDRA_IMAGE) .withConfigurationOverride("cassandra-empty-configuration") ) { - cassandraContainer.start(); + assertThatThrownBy(cassandraContainer::start).isInstanceOf(ContainerLaunchException.class); } } @Test - public void testInitScript() { + void testInitScript() { try ( CassandraContainer cassandraContainer = new CassandraContainer<>(CASSANDRA_IMAGE) .withInitScript("initial.cql") @@ -82,7 +83,7 @@ public void testInitScript() { } @Test - public void testInitScriptWithLegacyCassandra() { + void testInitScriptWithLegacyCassandra() { try ( CassandraContainer cassandraContainer = new CassandraContainer<>( DockerImageName.parse("cassandra:2.2.11") @@ -96,7 +97,7 @@ public void testInitScriptWithLegacyCassandra() { @SuppressWarnings("deprecation") // Using deprecated constructor for verification of backwards compatibility @Test - public void testCassandraQueryWaitStrategy() { + void testCassandraQueryWaitStrategy() { try ( CassandraContainer cassandraContainer = new CassandraContainer<>() .waitingFor(new CassandraQueryWaitStrategy()) @@ -109,7 +110,7 @@ public void testCassandraQueryWaitStrategy() { @SuppressWarnings("deprecation") // Using deprecated constructor for verification of backwards compatibility @Test - public void testCassandraGetCluster() { + void testCassandraGetCluster() { try (CassandraContainer cassandraContainer = new CassandraContainer<>()) { cassandraContainer.start(); ResultSet resultSet = performQuery(cassandraContainer.getCluster(), BASIC_QUERY); diff --git a/modules/cassandra/src/test/java/org/testcontainers/containers/CassandraDriver3Test.java b/modules/cassandra/src/test/java/org/testcontainers/containers/CassandraDriver3Test.java deleted file mode 100644 index 502f2ca8e67..00000000000 --- a/modules/cassandra/src/test/java/org/testcontainers/containers/CassandraDriver3Test.java +++ /dev/null @@ -1,37 +0,0 @@ -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.Rule; -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -public class CassandraDriver3Test { - - @Rule - public CassandraContainer cassandra = new CassandraContainer<>("cassandra:3.11.2"); - - @Test - public void testCassandraGetContactPoint() { - try ( - // cassandra { - CqlSession session = CqlSession - .builder() - .addContactPoint(this.cassandra.getContactPoint()) - .withLocalDatacenter(this.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("keyspace created").isNotNull(); - } - } -} diff --git a/modules/cassandra/src/test/java/org/testcontainers/containers/CassandraServer4Test.java b/modules/cassandra/src/test/java/org/testcontainers/containers/CassandraServer4Test.java deleted file mode 100644 index 1a8447e7036..00000000000 --- a/modules/cassandra/src/test/java/org/testcontainers/containers/CassandraServer4Test.java +++ /dev/null @@ -1,35 +0,0 @@ -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.Rule; -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -public class CassandraServer4Test { - - @Rule - public CassandraContainer cassandra = new CassandraContainer<>("cassandra:4.1.1"); - - @Test - public void testCassandraGetContactPoint() { - try ( - CqlSession session = CqlSession - .builder() - .addContactPoint(this.cassandra.getContactPoint()) - .withLocalDatacenter(this.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(); - } - } -} diff --git a/modules/cassandra/src/test/java/org/testcontainers/containers/CassandraDriver4Test.java b/modules/cassandra/src/test/java/org/testcontainers/containers/CompatibleCassandraImageTest.java similarity index 50% rename from modules/cassandra/src/test/java/org/testcontainers/containers/CassandraDriver4Test.java rename to modules/cassandra/src/test/java/org/testcontainers/containers/CompatibleCassandraImageTest.java index a4fb8c87b8e..aec27566beb 100644 --- a/modules/cassandra/src/test/java/org/testcontainers/containers/CassandraDriver4Test.java +++ b/modules/cassandra/src/test/java/org/testcontainers/containers/CompatibleCassandraImageTest.java @@ -3,23 +3,32 @@ 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.Rule; -import org.junit.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import static org.assertj.core.api.Assertions.assertThat; -public class CassandraDriver4Test { +public class CompatibleCassandraImageTest { - @Rule - public CassandraContainer cassandra = new CassandraContainer<>("cassandra:3.11.2"); + 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); + } + } - @Test - public void testCassandraGetContactPoint() { + private void assertCassandraFunctionality(CassandraContainer cassandra) { try ( CqlSession session = CqlSession .builder() - .addContactPoint(this.cassandra.getContactPoint()) - .withLocalDatacenter(this.cassandra.getLocalDatacenter()) + .addContactPoint(cassandra.getContactPoint()) + .withLocalDatacenter(cassandra.getLocalDatacenter()) .build() ) { session.execute( diff --git a/modules/cassandra/src/test/resources/cassandra-auth-required-configuration/cassandra.yaml b/modules/cassandra/src/test/resources/cassandra-auth-required-configuration/cassandra.yaml new file mode 100644 index 00000000000..7425881b8fe --- /dev/null +++ b/modules/cassandra/src/test/resources/cassandra-auth-required-configuration/cassandra.yaml @@ -0,0 +1,1233 @@ +# 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 diff --git a/modules/cassandra/src/test/resources/cassandra-ssl-configuration/cassandra.cer b/modules/cassandra/src/test/resources/cassandra-ssl-configuration/cassandra.cer new file mode 100644 index 00000000000..7a6aee6dec4 Binary files /dev/null and b/modules/cassandra/src/test/resources/cassandra-ssl-configuration/cassandra.cer differ diff --git a/modules/cassandra/src/test/resources/cassandra-ssl-configuration/cassandra.yaml b/modules/cassandra/src/test/resources/cassandra-ssl-configuration/cassandra.yaml new file mode 100644 index 00000000000..116ba3429cf --- /dev/null +++ b/modules/cassandra/src/test/resources/cassandra-ssl-configuration/cassandra.yaml @@ -0,0 +1,1233 @@ +# 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 diff --git a/modules/cassandra/src/test/resources/cassandra-ssl-configuration/keystore.p12 b/modules/cassandra/src/test/resources/cassandra-ssl-configuration/keystore.p12 new file mode 100644 index 00000000000..501e25cf967 Binary files /dev/null and b/modules/cassandra/src/test/resources/cassandra-ssl-configuration/keystore.p12 differ diff --git a/modules/cassandra/src/test/resources/cassandra-ssl-configuration/truststore.p12 b/modules/cassandra/src/test/resources/cassandra-ssl-configuration/truststore.p12 new file mode 100644 index 00000000000..57107fc3813 Binary files /dev/null and b/modules/cassandra/src/test/resources/cassandra-ssl-configuration/truststore.p12 differ diff --git a/modules/cassandra/src/test/resources/cassandra-test-configuration-example/cassandra.yaml b/modules/cassandra/src/test/resources/cassandra-test-configuration-example/cassandra.yaml index 5b57b2a8e58..426dea64771 100644 --- a/modules/cassandra/src/test/resources/cassandra-test-configuration-example/cassandra.yaml +++ b/modules/cassandra/src/test/resources/cassandra-test-configuration-example/cassandra.yaml @@ -250,7 +250,7 @@ commit_failure_policy: stop # # 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 +# 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 @@ -259,7 +259,7 @@ commit_failure_policy: stop # 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. +# 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 @@ -309,7 +309,7 @@ key_cache_save_period: 14400 # Fully off-heap row cache implementation (default). # # org.apache.cassandra.cache.SerializingCacheProvider -# This is the row cache implementation availabile +# This is the row cache implementation available # in previous releases of Cassandra. # row_cache_class_name: org.apache.cassandra.cache.OHCProvider @@ -444,7 +444,7 @@ concurrent_counter_writes: 32 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 +# 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 @@ -553,7 +553,7 @@ memtable_allocation_type: heap_buffers # 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 +# 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 @@ -778,7 +778,7 @@ auto_snapshot: true # 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 +# 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 @@ -1109,7 +1109,7 @@ 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 +# 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). # @@ -1143,7 +1143,7 @@ transparent_data_encryption_options: # 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. +# 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 diff --git a/modules/cassandra/src/test/resources/client-ssl/cassandra.cer.pem b/modules/cassandra/src/test/resources/client-ssl/cassandra.cer.pem new file mode 100644 index 00000000000..bafaa00317b --- /dev/null +++ b/modules/cassandra/src/test/resources/client-ssl/cassandra.cer.pem @@ -0,0 +1,26 @@ +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----- diff --git a/modules/cassandra/src/test/resources/client-ssl/cassandra.key.pem b/modules/cassandra/src/test/resources/client-ssl/cassandra.key.pem new file mode 100644 index 00000000000..4d291cf8852 --- /dev/null +++ b/modules/cassandra/src/test/resources/client-ssl/cassandra.key.pem @@ -0,0 +1,32 @@ +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----- diff --git a/modules/cassandra/src/test/resources/initial-with-error.cql b/modules/cassandra/src/test/resources/initial-with-error.cql new file mode 100644 index 00000000000..4f0e11d721b --- /dev/null +++ b/modules/cassandra/src/test/resources/initial-with-error.cql @@ -0,0 +1,6 @@ +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); diff --git a/modules/chromadb/build.gradle b/modules/chromadb/build.gradle new file mode 100644 index 00000000000..ec41def56df --- /dev/null +++ b/modules/chromadb/build.gradle @@ -0,0 +1,7 @@ +description = "Testcontainers :: ChromaDB" + +dependencies { + api project(':testcontainers') + + testImplementation 'io.rest-assured:rest-assured:5.5.7' +} diff --git a/modules/chromadb/src/main/java/org/testcontainers/chromadb/ChromaDBContainer.java b/modules/chromadb/src/main/java/org/testcontainers/chromadb/ChromaDBContainer.java new file mode 100644 index 00000000000..af6c3df33fc --- /dev/null +++ b/modules/chromadb/src/main/java/org/testcontainers/chromadb/ChromaDBContainer.java @@ -0,0 +1,56 @@ +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; + } +} diff --git a/modules/chromadb/src/test/java/org/testcontainers/chromadb/ChromaDBContainerTest.java b/modules/chromadb/src/test/java/org/testcontainers/chromadb/ChromaDBContainerTest.java new file mode 100644 index 00000000000..579c75f9f36 --- /dev/null +++ b/modules/chromadb/src/test/java/org/testcontainers/chromadb/ChromaDBContainerTest.java @@ -0,0 +1,48 @@ +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); + } + } +} diff --git a/modules/dynalite/src/test/resources/logback-test.xml b/modules/chromadb/src/test/resources/logback-test.xml similarity index 100% rename from modules/dynalite/src/test/resources/logback-test.xml rename to modules/chromadb/src/test/resources/logback-test.xml diff --git a/modules/clickhouse/build.gradle b/modules/clickhouse/build.gradle index d013575def5..59ea169583c 100644 --- a/modules/clickhouse/build.gradle +++ b/modules/clickhouse/build.gradle @@ -2,9 +2,15 @@ description = "Testcontainers :: JDBC :: ClickHouse" dependencies { api project(':testcontainers') - api project(':jdbc') + api project(':testcontainers-jdbc') - testImplementation project(':jdbc-test') - testRuntimeOnly 'ru.yandex.clickhouse:clickhouse-jdbc:0.3.2' - testImplementation 'org.assertj:assertj-core:3.25.1' + compileOnly project(':testcontainers-r2dbc') + compileOnly(group: 'com.clickhouse', name: 'clickhouse-r2dbc', version: '0.9.8', classifier: 'http') + + testImplementation project(':testcontainers-jdbc-test') + testRuntimeOnly(group: 'com.clickhouse', name: 'clickhouse-jdbc', version: '0.9.8', classifier: 'all') + + testImplementation 'com.clickhouse:client-v2:0.9.8' + testImplementation testFixtures(project(':testcontainers-r2dbc')) + testRuntimeOnly(group: 'com.clickhouse', name: 'clickhouse-r2dbc', version: '0.9.8', classifier: 'http') } diff --git a/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseContainer.java b/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseContainer.java index ffa1307204a..02a02267ec5 100644 --- a/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseContainer.java +++ b/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseContainer.java @@ -1,7 +1,7 @@ package org.testcontainers.clickhouse; import org.testcontainers.containers.JdbcDatabaseContainer; -import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; +import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import java.time.Duration; @@ -21,25 +21,31 @@ */ public class ClickHouseContainer extends JdbcDatabaseContainer { - private static final String NAME = "clickhouse"; + static final String CLICKHOUSE_CLICKHOUSE_SERVER = "clickhouse/clickhouse-server"; - private static final DockerImageName CLICKHOUSE_IMAGE_NAME = DockerImageName.parse("clickhouse/clickhouse-server"); + private static final DockerImageName CLICKHOUSE_IMAGE_NAME = DockerImageName.parse(CLICKHOUSE_CLICKHOUSE_SERVER); - private static final Integer HTTP_PORT = 8123; + static final Integer HTTP_PORT = 8123; - private static final Integer NATIVE_PORT = 9000; + static final Integer NATIVE_PORT = 9000; - private static final String DRIVER_CLASS_NAME = "com.clickhouse.jdbc.ClickHouseDriver"; + private static final String LEGACY_V1_DRIVER_CLASS_NAME = "com.clickhouse.jdbc.ClickHouseDriver"; - private static final String JDBC_URL_PREFIX = "jdbc:" + NAME + "://"; + 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"; + private String username = DEFAULT_USER; - private String password = ""; + private String password = DEFAULT_PASSWORD; public ClickHouseContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); @@ -50,11 +56,14 @@ public ClickHouseContainer(final DockerImageName dockerImageName) { dockerImageName.assertCompatibleWith(CLICKHOUSE_IMAGE_NAME); addExposedPorts(HTTP_PORT, NATIVE_PORT); - this.waitStrategy = - new HttpWaitStrategy() + waitingFor( + Wait + .forHttp("/") + .forPort(HTTP_PORT) .forStatusCode(200) .forResponsePredicate("Ok."::equals) - .withStartupTimeout(Duration.ofMinutes(1)); + .withStartupTimeout(Duration.ofMinutes(1)) + ); } @Override @@ -71,7 +80,12 @@ public Set getLivenessCheckPortNumbers() { @Override public String getDriverClassName() { - return DRIVER_CLASS_NAME; + try { + Class.forName(DRIVER_CLASS_NAME); + return DRIVER_CLASS_NAME; + } catch (ClassNotFoundException e) { + return LEGACY_V1_DRIVER_CLASS_NAME; + } } @Override @@ -87,6 +101,10 @@ public String getJdbcUrl() { ); } + public String getHttpUrl() { + return "http://" + getHost() + ":" + getMappedPort(HTTP_PORT); + } + @Override public String getUsername() { return username; @@ -97,6 +115,11 @@ public String getPassword() { return password; } + @Override + public String getDatabaseName() { + return databaseName; + } + @Override public String getTestQueryString() { return TEST_QUERY; @@ -119,4 +142,9 @@ public ClickHouseContainer withDatabaseName(String databaseName) { this.databaseName = databaseName; return this; } + + @Override + protected void waitUntilContainerStarted() { + getWaitStrategy().waitUntilReady(this); + } } diff --git a/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainer.java b/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainer.java new file mode 100644 index 00000000000..be6d8af67c2 --- /dev/null +++ b/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainer.java @@ -0,0 +1,48 @@ +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(); + } +} diff --git a/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainerProvider.java b/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainerProvider.java new file mode 100644 index 00000000000..d2005f15bb1 --- /dev/null +++ b/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainerProvider.java @@ -0,0 +1,46 @@ +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()); + } +} diff --git a/modules/clickhouse/src/main/java/org/testcontainers/containers/ClickHouseProvider.java b/modules/clickhouse/src/main/java/org/testcontainers/containers/ClickHouseProvider.java index 80fb71bd5da..250631c1500 100644 --- a/modules/clickhouse/src/main/java/org/testcontainers/containers/ClickHouseProvider.java +++ b/modules/clickhouse/src/main/java/org/testcontainers/containers/ClickHouseProvider.java @@ -1,16 +1,24 @@ 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(ClickHouseContainer.NAME); + return databaseType.equals("clickhouse"); + } + + @Override + public JdbcDatabaseContainer newInstance() { + return newInstance(DEFAULT_TAG); } @Override - public JdbcDatabaseContainer newInstance(String tag) { - return new ClickHouseContainer(DockerImageName.parse(ClickHouseContainer.IMAGE).withTag(tag)); + public JdbcDatabaseContainer newInstance(String tag) { + return new ClickHouseContainer(DockerImageName.parse("clickhouse/clickhouse-server").withTag(tag)); } } diff --git a/modules/clickhouse/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider b/modules/clickhouse/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider new file mode 100644 index 00000000000..4898be97d99 --- /dev/null +++ b/modules/clickhouse/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider @@ -0,0 +1 @@ +org.testcontainers.clickhouse.ClickHouseR2DBCDatabaseContainerProvider diff --git a/modules/clickhouse/src/test/java/org/testcontainers/ClickhouseTestImages.java b/modules/clickhouse/src/test/java/org/testcontainers/ClickhouseTestImages.java index a6707705ff8..eff4e19f70b 100644 --- a/modules/clickhouse/src/test/java/org/testcontainers/ClickhouseTestImages.java +++ b/modules/clickhouse/src/test/java/org/testcontainers/ClickhouseTestImages.java @@ -3,6 +3,7 @@ import org.testcontainers.utility.DockerImageName; public interface ClickhouseTestImages { - DockerImageName YANDEX_CLICKHOUSE_IMAGE = DockerImageName.parse("yandex/clickhouse-server:18.10.3"); - DockerImageName CLICKHOUSE_IMAGE = DockerImageName.parse("clickhouse/clickhouse-server:21.9.2-alpine"); + 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"); } diff --git a/modules/clickhouse/src/test/java/org/testcontainers/clickhouse/ClickHouseContainerTest.java b/modules/clickhouse/src/test/java/org/testcontainers/clickhouse/ClickHouseContainerTest.java index e440af648af..0f9eb3ef6a3 100644 --- a/modules/clickhouse/src/test/java/org/testcontainers/clickhouse/ClickHouseContainerTest.java +++ b/modules/clickhouse/src/test/java/org/testcontainers/clickhouse/ClickHouseContainerTest.java @@ -1,18 +1,29 @@ package org.testcontainers.clickhouse; -import org.junit.Test; +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; -public class ClickHouseContainerTest extends AbstractContainerDatabaseTest { +class ClickHouseContainerTest extends AbstractContainerDatabaseTest { @Test - public void testSimple() throws SQLException { - try (ClickHouseContainer clickhouse = new ClickHouseContainer("clickhouse/clickhouse-server:21.9.2-alpine")) { + void testSimple() throws SQLException { + try ( // container { + ClickHouseContainer clickhouse = new ClickHouseContainer("clickhouse/clickhouse-server:21.11-alpine") + // } + ) { clickhouse.start(); ResultSet resultSet = performQuery(clickhouse, "SELECT 1"); @@ -23,13 +34,14 @@ public void testSimple() throws SQLException { } @Test - public void customCredentialsWithUrlParams() throws SQLException { + void customCredentialsWithUrlParams() throws SQLException { try ( - ClickHouseContainer clickhouse = new ClickHouseContainer("clickhouse/clickhouse-server:21.9.2-alpine") - .withUsername("test") - .withPassword("test") + ClickHouseContainer clickhouse = new ClickHouseContainer("clickhouse/clickhouse-server:21.11.2-alpine") + .withUsername("default") + .withPassword("") .withDatabaseName("test") - .withUrlParam("max_result_rows", "5") + // The new driver uses the prefix `clickhouse_setting_` for session settings + .withUrlParam("clickhouse_setting_max_result_rows", "5") ) { clickhouse.start(); @@ -42,4 +54,39 @@ public void customCredentialsWithUrlParams() throws SQLException { 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(); + } + } } diff --git a/modules/clickhouse/src/test/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainerTest.java b/modules/clickhouse/src/test/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainerTest.java new file mode 100644 index 00000000000..000fff8983a --- /dev/null +++ b/modules/clickhouse/src/test/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainerTest.java @@ -0,0 +1,22 @@ +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"); + } +} diff --git a/modules/clickhouse/src/test/java/org/testcontainers/jdbc/clickhouse/ClickhouseJDBCDriverTest.java b/modules/clickhouse/src/test/java/org/testcontainers/jdbc/clickhouse/ClickhouseJDBCDriverTest.java index 056aace7772..0a85a26899a 100644 --- a/modules/clickhouse/src/test/java/org/testcontainers/jdbc/clickhouse/ClickhouseJDBCDriverTest.java +++ b/modules/clickhouse/src/test/java/org/testcontainers/jdbc/clickhouse/ClickhouseJDBCDriverTest.java @@ -1,16 +1,12 @@ package org.testcontainers.jdbc.clickhouse; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; -@RunWith(Parameterized.class) -public class ClickhouseJDBCDriverTest extends AbstractJDBCDriverTest { +class ClickhouseJDBCDriverTest extends AbstractJDBCDriverTest { - @Parameterized.Parameters(name = "{index} - {0}") public static Iterable data() { return Arrays.asList( new Object[][] { // diff --git a/modules/clickhouse/src/test/java/org/testcontainers/junit/clickhouse/SimpleClickhouseTest.java b/modules/clickhouse/src/test/java/org/testcontainers/junit/clickhouse/SimpleClickhouseTest.java index 8f81c88bfc0..c9bd9e917f5 100644 --- a/modules/clickhouse/src/test/java/org/testcontainers/junit/clickhouse/SimpleClickhouseTest.java +++ b/modules/clickhouse/src/test/java/org/testcontainers/junit/clickhouse/SimpleClickhouseTest.java @@ -1,38 +1,20 @@ package org.testcontainers.junit.clickhouse; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; +import org.junit.jupiter.api.Test; import org.testcontainers.ClickhouseTestImages; import org.testcontainers.containers.ClickHouseContainer; 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; -@RunWith(Parameterized.class) -public class SimpleClickhouseTest extends AbstractContainerDatabaseTest { - - private final DockerImageName imageName; - - public SimpleClickhouseTest(DockerImageName imageName) { - this.imageName = imageName; - } - - @Parameterized.Parameters(name = "{0}") - public static Object[][] data() { - return new Object[][] { // - { ClickhouseTestImages.CLICKHOUSE_IMAGE }, - { ClickhouseTestImages.YANDEX_CLICKHOUSE_IMAGE }, - }; - } +class SimpleClickhouseTest extends AbstractContainerDatabaseTest { @Test public void testSimple() throws SQLException { - try (ClickHouseContainer clickhouse = new ClickHouseContainer(this.imageName)) { + try (ClickHouseContainer clickhouse = new ClickHouseContainer(ClickhouseTestImages.CLICKHOUSE_IMAGE)) { clickhouse.start(); ResultSet resultSet = performQuery(clickhouse, "SELECT 1"); diff --git a/modules/cockroachdb/build.gradle b/modules/cockroachdb/build.gradle index 82b55fd7e5e..275e69eef3d 100644 --- a/modules/cockroachdb/build.gradle +++ b/modules/cockroachdb/build.gradle @@ -1,9 +1,9 @@ description = "Testcontainers :: JDBC :: CockroachDB" dependencies { - api project(':jdbc') + api project(':testcontainers-jdbc') - testImplementation project(':jdbc-test') - testRuntimeOnly 'org.postgresql:postgresql:42.7.1' - testImplementation 'org.assertj:assertj-core:3.25.1' + testRuntimeOnly 'org.postgresql:postgresql:42.7.10' + + testImplementation project(':testcontainers-jdbc-test') } diff --git a/modules/cockroachdb/src/main/java/org/testcontainers/cockroachdb/CockroachContainer.java b/modules/cockroachdb/src/main/java/org/testcontainers/cockroachdb/CockroachContainer.java new file mode 100644 index 00000000000..0d49e8cdaf7 --- /dev/null +++ b/modules/cockroachdb/src/main/java/org/testcontainers/cockroachdb/CockroachContainer.java @@ -0,0 +1,158 @@ +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); + } +} diff --git a/modules/cockroachdb/src/main/java/org/testcontainers/containers/CockroachContainer.java b/modules/cockroachdb/src/main/java/org/testcontainers/containers/CockroachContainer.java index 3a4ed8586c9..461b0467767 100644 --- a/modules/cockroachdb/src/main/java/org/testcontainers/containers/CockroachContainer.java +++ b/modules/cockroachdb/src/main/java/org/testcontainers/containers/CockroachContainer.java @@ -1,6 +1,7 @@ package org.testcontainers.containers; -import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.containers.wait.strategy.WaitAllStrategy; import org.testcontainers.utility.ComparableVersion; import org.testcontainers.utility.DockerImageName; @@ -16,7 +17,10 @@ *
  • 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"); @@ -66,19 +70,31 @@ public CockroachContainer(final String dockerImageName) { public CockroachContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); - isVersionGreaterThanOrEqualTo221 = isVersionGreaterThanOrEqualTo221(dockerImageName); + this.isVersionGreaterThanOrEqualTo221 = isVersionGreaterThanOrEqualTo221(dockerImageName); - withExposedPorts(REST_API_PORT, DB_PORT); - waitingFor( - new HttpWaitStrategy() - .forPath("/health") - .forPort(REST_API_PORT) - .forStatusCode(200) - .withStartupTimeout(Duration.ofMinutes(1)) + WaitAllStrategy waitStrategy = new WaitAllStrategy(); + waitStrategy.withStrategy( + Wait.forHttp("/health").forPort(REST_API_PORT).forStatusCode(200).withStartupTimeout(Duration.ofMinutes(1)) ); + if (this.isVersionGreaterThanOrEqualTo221) { + waitStrategy.withStrategy(Wait.forSuccessfulCommand("[ -f ./init_success ] || { exit 1; }")); + } + + withExposedPorts(REST_API_PORT, DB_PORT); + waitingFor(waitStrategy); withCommand("start-single-node --insecure"); } + @Override + protected void configure() { + withEnv("COCKROACH_USER", this.username); + withEnv("COCKROACH_PASSWORD", this.password); + if (this.password != null && !this.password.isEmpty()) { + withCommand("start-single-node"); + } + withEnv("COCKROACH_DATABASE", this.databaseName); + } + @Override public String getDriverClassName() { return JDBC_DRIVER_CLASS_NAME; @@ -123,21 +139,21 @@ public String getTestQueryString() { public CockroachContainer withUsername(String username) { validateIfVersionSupportsUsernameOrPasswordOrDatabase("username"); this.username = username; - return withEnv("COCKROACH_USER", username); + return this; } @Override public CockroachContainer withPassword(String password) { validateIfVersionSupportsUsernameOrPasswordOrDatabase("password"); this.password = password; - return withEnv("COCKROACH_PASSWORD", password).withCommand("start-single-node"); + return this; } @Override public CockroachContainer withDatabaseName(final String databaseName) { validateIfVersionSupportsUsernameOrPasswordOrDatabase("databaseName"); this.databaseName = databaseName; - return withEnv("COCKROACH_DATABASE", databaseName); + return this; } private boolean isVersionGreaterThanOrEqualTo221(DockerImageName dockerImageName) { @@ -152,4 +168,9 @@ private void validateIfVersionSupportsUsernameOrPasswordOrDatabase(String parame ); } } + + @Override + protected void waitUntilContainerStarted() { + getWaitStrategy().waitUntilReady(this); + } } diff --git a/modules/cockroachdb/src/test/java/org/testcontainers/CockroachDBTestImages.java b/modules/cockroachdb/src/test/java/org/testcontainers/CockroachDBTestImages.java index 6e18781f2c4..0e977cfd065 100644 --- a/modules/cockroachdb/src/test/java/org/testcontainers/CockroachDBTestImages.java +++ b/modules/cockroachdb/src/test/java/org/testcontainers/CockroachDBTestImages.java @@ -4,11 +4,4 @@ public interface CockroachDBTestImages { DockerImageName COCKROACHDB_IMAGE = DockerImageName.parse("cockroachdb/cockroach:v22.2.3"); - DockerImageName FIRST_COCKROACHDB_IMAGE_WITH_ENV_VARS_SUPPORT = DockerImageName.parse( - "cockroachdb/cockroach:v22.1.0" - ); - - DockerImageName COCKROACHDB_IMAGE_WITH_ENV_VARS_UNSUPPORTED = DockerImageName.parse( - "cockroachdb/cockroach:v21.2.17" - ); } diff --git a/modules/cockroachdb/src/test/java/org/testcontainers/junit/cockroachdb/SimpleCockroachDBTest.java b/modules/cockroachdb/src/test/java/org/testcontainers/cockroachdb/CockroachContainerTest.java similarity index 60% rename from modules/cockroachdb/src/test/java/org/testcontainers/junit/cockroachdb/SimpleCockroachDBTest.java rename to modules/cockroachdb/src/test/java/org/testcontainers/cockroachdb/CockroachContainerTest.java index fe9ef5fb81c..2f4710eae35 100644 --- a/modules/cockroachdb/src/test/java/org/testcontainers/junit/cockroachdb/SimpleCockroachDBTest.java +++ b/modules/cockroachdb/src/test/java/org/testcontainers/cockroachdb/CockroachContainerTest.java @@ -1,9 +1,9 @@ -package org.testcontainers.junit.cockroachdb; +package org.testcontainers.cockroachdb; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.CockroachDBTestImages; -import org.testcontainers.containers.CockroachContainer; import org.testcontainers.db.AbstractContainerDatabaseTest; +import org.testcontainers.images.builder.Transferable; import java.sql.ResultSet; import java.sql.SQLException; @@ -11,17 +11,19 @@ import java.util.logging.LogManager; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -public class SimpleCockroachDBTest extends AbstractContainerDatabaseTest { +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 - public void testSimple() throws SQLException { - try (CockroachContainer cockroach = new CockroachContainer(CockroachDBTestImages.COCKROACHDB_IMAGE)) { + void testSimple() throws SQLException { + try ( // container { + CockroachContainer cockroach = new CockroachContainer("cockroachdb/cockroach:v26.1.1") + // } + ) { cockroach.start(); ResultSet resultSet = performQuery(cockroach, "SELECT 1"); @@ -32,7 +34,7 @@ public void testSimple() throws SQLException { } @Test - public void testExplicitInitScript() throws SQLException { + void testExplicitInitScript() throws SQLException { try ( CockroachContainer cockroach = new CockroachContainer(CockroachDBTestImages.COCKROACHDB_IMAGE) .withInitScript("somepath/init_postgresql.sql") @@ -47,7 +49,7 @@ public void testExplicitInitScript() throws SQLException { } @Test - public void testWithAdditionalUrlParamInJdbcUrl() { + void testWithAdditionalUrlParamInJdbcUrl() { CockroachContainer cockroach = new CockroachContainer(CockroachDBTestImages.COCKROACHDB_IMAGE) .withUrlParam("sslmode", "disable") .withUrlParam("application_name", "cockroach"); @@ -66,11 +68,9 @@ public void testWithAdditionalUrlParamInJdbcUrl() { } @Test - public void testWithUsernamePasswordDatabase() throws SQLException { + void testWithUsernamePasswordDatabase() throws SQLException { try ( - CockroachContainer cockroach = new CockroachContainer( - CockroachDBTestImages.FIRST_COCKROACHDB_IMAGE_WITH_ENV_VARS_SUPPORT - ) + CockroachContainer cockroach = new CockroachContainer(CockroachDBTestImages.COCKROACHDB_IMAGE) .withUsername("test_user") .withPassword("test_password") .withDatabaseName("test_database") @@ -88,21 +88,23 @@ public void testWithUsernamePasswordDatabase() throws SQLException { } @Test - public void testAnExceptionIsThrownWhenImageDoesNotSupportEnvVars() { - CockroachContainer cockroachContainer = new CockroachContainer( - CockroachDBTestImages.COCKROACHDB_IMAGE_WITH_ENV_VARS_UNSUPPORTED - ); - - assertThatThrownBy(() -> cockroachContainer.withUsername("test_user")) - .isInstanceOf(UnsupportedOperationException.class) - .withFailMessage("Setting a username in not supported in the versions below 22.1.0"); - - assertThatThrownBy(() -> cockroachContainer.withPassword("test_password")) - .isInstanceOf(UnsupportedOperationException.class) - .withFailMessage("Setting a password in not supported in the versions below 22.1.0"); - - assertThatThrownBy(() -> cockroachContainer.withDatabaseName("test_database")) - .isInstanceOf(UnsupportedOperationException.class) - .withFailMessage("Setting a databaseName in not supported in the versions below 22.1.0"); + void testInitializationScript() throws SQLException { + String sql = + "USE postgres; \n" + + "CREATE TABLE bar (foo VARCHAR(255)); \n" + + "INSERT INTO bar (foo) VALUES ('hello world');"; + + try ( + CockroachContainer cockroach = new CockroachContainer(CockroachDBTestImages.COCKROACHDB_IMAGE) + .withCopyToContainer(Transferable.of(sql), "/docker-entrypoint-initdb.d/init.sql") + .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) + ) { // CockroachDB is expected to be compatible with Postgres + cockroach.start(); + + ResultSet resultSet = performQuery(cockroach, "SELECT foo FROM bar"); + + String firstColumnValue = resultSet.getString(1); + assertThat(firstColumnValue).as("Value from init script should equal real value").isEqualTo("hello world"); + } } } diff --git a/modules/cockroachdb/src/test/java/org/testcontainers/jdbc/cockroachdb/CockroachDBJDBCDriverTest.java b/modules/cockroachdb/src/test/java/org/testcontainers/jdbc/cockroachdb/CockroachDBJDBCDriverTest.java index 86aaf6b4f63..097aeef005b 100644 --- a/modules/cockroachdb/src/test/java/org/testcontainers/jdbc/cockroachdb/CockroachDBJDBCDriverTest.java +++ b/modules/cockroachdb/src/test/java/org/testcontainers/jdbc/cockroachdb/CockroachDBJDBCDriverTest.java @@ -1,20 +1,16 @@ package org.testcontainers.jdbc.cockroachdb; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; -@RunWith(Parameterized.class) -public class CockroachDBJDBCDriverTest extends AbstractJDBCDriverTest { +class CockroachDBJDBCDriverTest extends AbstractJDBCDriverTest { - @Parameterized.Parameters(name = "{index} - {0}") public static Iterable data() { return Arrays.asList( new Object[][] { // - { "jdbc:tc:cockroach://hostname/databasename", EnumSet.noneOf(Options.class) }, + { "jdbc:tc:cockroach:v22.2.3://hostname/databasename", EnumSet.noneOf(Options.class) }, } ); } diff --git a/modules/consul/build.gradle b/modules/consul/build.gradle index 9c1195525ff..f496a86d6fd 100644 --- a/modules/consul/build.gradle +++ b/modules/consul/build.gradle @@ -4,6 +4,5 @@ dependencies { api project(':testcontainers') testImplementation 'com.ecwid.consul:consul-api:1.4.5' - testImplementation 'io.rest-assured:rest-assured:5.4.0' - testImplementation 'org.assertj:assertj-core:3.25.1' + testImplementation 'io.rest-assured:rest-assured:5.5.7' } diff --git a/modules/consul/src/main/java/org/testcontainers/consul/ConsulContainer.java b/modules/consul/src/main/java/org/testcontainers/consul/ConsulContainer.java index 43bbada877f..2922d9711ae 100644 --- a/modules/consul/src/main/java/org/testcontainers/consul/ConsulContainer.java +++ b/modules/consul/src/main/java/org/testcontainers/consul/ConsulContainer.java @@ -91,7 +91,7 @@ private void runConsulCommands() { /** * Run consul commands using the consul cli. * - * Useful for enableing more secret engines like: + * Useful for enabling more secret engines like: *
      *     .withConsulCommand("secrets enable pki")
      *     .withConsulCommand("secrets enable transit")
diff --git a/modules/consul/src/test/java/org/testcontainers/consul/ConsulContainerTest.java b/modules/consul/src/test/java/org/testcontainers/consul/ConsulContainerTest.java
index 7f1c6d1c207..6e02bff51e8 100644
--- a/modules/consul/src/test/java/org/testcontainers/consul/ConsulContainerTest.java
+++ b/modules/consul/src/test/java/org/testcontainers/consul/ConsulContainerTest.java
@@ -4,8 +4,9 @@
 import com.ecwid.consul.v1.Response;
 import com.ecwid.consul.v1.kv.model.GetValue;
 import io.restassured.RestAssured;
-import org.junit.ClassRule;
-import org.junit.Test;
+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;
@@ -16,25 +17,30 @@
 
 import static org.assertj.core.api.Assertions.assertThat;
 
-/**
- * This test shows the pattern to use the ConsulContainer @ClassRule for a junit test. It also has tests that ensure
- * the properties were added correctly by reading from Consul with the CLI and over HTTP.
- */
-public class ConsulContainerTest {
+class ConsulContainerTest {
 
-    @ClassRule
-    public static ConsulContainer consulContainer = new ConsulContainer("hashicorp/consul:1.15")
+    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
-    public void readFirstPropertyPathWithCli() throws IOException, InterruptedException {
-        GenericContainer.ExecResult result = consulContainer.execInContainer("consul", "kv", "get", "config/testing1");
+    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
-    public void readFirstSecretPathOverHttpApi() {
+    void readFirstSecretPathOverHttpApi() {
         io.restassured.response.Response response = RestAssured
             .given()
             .when()
@@ -46,11 +52,8 @@ public void readFirstSecretPathOverHttpApi() {
     }
 
     @Test
-    public void writeAndReadMultipleValuesUsingClient() {
-        final ConsulClient consulClient = new ConsulClient(
-            consulContainer.getHost(),
-            consulContainer.getFirstMappedPort()
-        );
+    void writeAndReadMultipleValuesUsingClient() {
+        final ConsulClient consulClient = new ConsulClient(consul.getHost(), consul.getFirstMappedPort());
 
         final Map properties = new HashMap<>();
         properties.put("value", "world");
@@ -70,6 +73,6 @@ public void writeAndReadMultipleValuesUsingClient() {
     }
 
     private String getHostAndPort() {
-        return consulContainer.getHost() + ":" + consulContainer.getMappedPort(8500);
+        return consul.getHost() + ":" + consul.getMappedPort(8500);
     }
 }
diff --git a/modules/couchbase/build.gradle b/modules/couchbase/build.gradle
index 032aacecfed..068e334e2f1 100644
--- a/modules/couchbase/build.gradle
+++ b/modules/couchbase/build.gradle
@@ -3,9 +3,8 @@ description = "Testcontainers :: Couchbase"
 dependencies {
     api project(':testcontainers')
     // TODO use JDK's HTTP client and/or Apache HttpClient5
-    shaded 'com.squareup.okhttp3:okhttp:4.12.0'
+    shaded 'com.squareup.okhttp3:okhttp:5.5.0'
 
-    testImplementation 'com.couchbase.client:java-client:3.5.2'
-    testImplementation 'org.awaitility:awaitility:4.2.0'
-    testImplementation 'org.assertj:assertj-core:3.25.1'
+    testImplementation 'com.couchbase.client:java-client:3.11.2'
+    testImplementation 'org.awaitility:awaitility:4.3.0'
 }
diff --git a/modules/couchbase/src/main/java/org/testcontainers/couchbase/CouchbaseContainer.java b/modules/couchbase/src/main/java/org/testcontainers/couchbase/CouchbaseContainer.java
index 5e9a4b45f4d..14997d253af 100644
--- a/modules/couchbase/src/main/java/org/testcontainers/couchbase/CouchbaseContainer.java
+++ b/modules/couchbase/src/main/java/org/testcontainers/couchbase/CouchbaseContainer.java
@@ -92,8 +92,6 @@ public class CouchbaseContainer extends GenericContainer {
 
     private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("couchbase/server");
 
-    private static final String DEFAULT_TAG = "6.5.1";
-
     private static final ObjectMapper MAPPER = new ObjectMapper();
 
     private static final OkHttpClient HTTP_CLIENT = new OkHttpClient();
@@ -123,14 +121,7 @@ public class CouchbaseContainer extends GenericContainer {
 
     private boolean isEnterprise = false;
 
-    /**
-     * Creates a new couchbase container with the default image and version.
-     * @deprecated use {@link #CouchbaseContainer(DockerImageName)} instead
-     */
-    @Deprecated
-    public CouchbaseContainer() {
-        this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG));
-    }
+    private boolean hasTlsPorts = false;
 
     /**
      * Creates a new couchbase container with the specified image name.
@@ -339,12 +330,20 @@ private void exposePorts() {
         }
     }
 
+    @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);
@@ -356,6 +355,13 @@ protected void containerIsStarting(final InspectContainerResponse containerInfo)
         }
     }
 
+    @Override
+    protected void containerIsStarted(InspectContainerResponse containerInfo, boolean reused) {
+        if (!reused) {
+            this.containerIsStarted(containerInfo);
+        }
+    }
+
     @Override
     protected void containerIsStarted(InspectContainerResponse containerInfo) {
         timePhase("createBuckets", this::createBuckets);
@@ -394,6 +400,31 @@ private void initializeIsEnterprise() {
         }
     }
 
+    /**
+     * 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. *

@@ -503,33 +534,45 @@ private void configureExternalPorts() { final FormBody.Builder builder = new FormBody.Builder(); builder.add("hostname", getHost()); builder.add("mgmt", Integer.toString(getMappedPort(MGMT_PORT))); - builder.add("mgmtSSL", Integer.toString(getMappedPort(MGMT_SSL_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("kvSSL", Integer.toString(getMappedPort(KV_SSL_PORT))); builder.add("capi", Integer.toString(getMappedPort(VIEW_PORT))); - builder.add("capiSSL", Integer.toString(getMappedPort(VIEW_SSL_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))); - builder.add("n1qlSSL", Integer.toString(getMappedPort(QUERY_SSL_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))); - builder.add("ftsSSL", Integer.toString(getMappedPort(SEARCH_SSL_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))); - builder.add("cbasSSL", Integer.toString(getMappedPort(ANALYTICS_SSL_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))); - builder.add("eventingSSL", Integer.toString(getMappedPort(EVENTING_SSL_PORT))); + if (hasTlsPorts) { + builder.add("eventingSSL", Integer.toString(getMappedPort(EVENTING_SSL_PORT))); + } } @Cleanup diff --git a/modules/couchbase/src/test/java/org/testcontainers/couchbase/CouchbaseContainerTest.java b/modules/couchbase/src/test/java/org/testcontainers/couchbase/CouchbaseContainerTest.java index 691dc1307ef..b973d5eff2a 100644 --- a/modules/couchbase/src/test/java/org/testcontainers/couchbase/CouchbaseContainerTest.java +++ b/modules/couchbase/src/test/java/org/testcontainers/couchbase/CouchbaseContainerTest.java @@ -20,9 +20,8 @@ import com.couchbase.client.java.Cluster; import com.couchbase.client.java.Collection; import com.couchbase.client.java.json.JsonObject; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.ContainerLaunchException; -import org.testcontainers.utility.DockerImageName; import java.time.Duration; import java.util.function.Consumer; @@ -31,53 +30,45 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; -public class CouchbaseContainerTest { +class CouchbaseContainerTest { - private static final DockerImageName COUCHBASE_IMAGE_ENTERPRISE = DockerImageName.parse( - "couchbase/server:enterprise-7.0.3" - ); + private static final String COUCHBASE_IMAGE_ENTERPRISE = "couchbase/server:enterprise-7.0.3"; - private static final DockerImageName COUCHBASE_IMAGE_COMMUNITY = DockerImageName.parse( - "couchbase/server:community-7.0.2" - ); + private static final String COUCHBASE_IMAGE_ENTERPRISE_RECENT = "couchbase/server:enterprise-7.6.2"; - @Test - public void testBasicContainerUsageForEnterpriseContainer() { - // bucket_definition { - BucketDefinition bucketDefinition = new BucketDefinition("mybucket"); - // } - - try ( - // container_definition { - CouchbaseContainer container = new CouchbaseContainer(COUCHBASE_IMAGE_ENTERPRISE) - .withBucket(bucketDefinition) - // } - ) { - setUpClient( - container, - cluster -> { - Bucket bucket = cluster.bucket(bucketDefinition.getName()); - bucket.waitUntilReady(Duration.ofSeconds(10L)); + private static final String COUCHBASE_IMAGE_COMMUNITY = "couchbase/server:community-7.0.2"; - Collection collection = bucket.defaultCollection(); + private static final String COUCHBASE_IMAGE_COMMUNITY_RECENT = "couchbase/server:community-7.6.2"; - collection.upsert("foo", JsonObject.create().put("key", "value")); + @Test + void testBasicContainerUsageForEnterpriseContainer() { + testBasicContainerUsage(COUCHBASE_IMAGE_ENTERPRISE); + } - JsonObject fooObject = collection.get("foo").contentAsObject(); + @Test + void testBasicContainerUsageForEnterpriseContainerRecent() { + testBasicContainerUsage(COUCHBASE_IMAGE_ENTERPRISE_RECENT); + } - assertThat(fooObject.getString("key")).isEqualTo("value"); - } - ); - } + @Test + void testBasicContainerUsageForCommunityContainer() { + testBasicContainerUsage(COUCHBASE_IMAGE_COMMUNITY); } @Test - public void testBasicContainerUsageForCommunityContainer() { + void testBasicContainerUsageForCommunityContainerRecent() { + testBasicContainerUsage(COUCHBASE_IMAGE_COMMUNITY_RECENT); + } + + private void testBasicContainerUsage(String couchbaseImage) { + // bucket_definition { BucketDefinition bucketDefinition = new BucketDefinition("mybucket"); + // } try ( - CouchbaseContainer container = new CouchbaseContainer(COUCHBASE_IMAGE_COMMUNITY) - .withBucket(bucketDefinition) + // container_definition { + CouchbaseContainer container = new CouchbaseContainer(couchbaseImage).withBucket(bucketDefinition) + // } ) { setUpClient( container, @@ -98,7 +89,7 @@ public void testBasicContainerUsageForCommunityContainer() { } @Test - public void testBucketIsFlushableIfEnabled() { + void testBucketIsFlushableIfEnabled() { BucketDefinition bucketDefinition = new BucketDefinition("mybucket").withFlushEnabled(true); try ( @@ -128,7 +119,7 @@ public void testBucketIsFlushableIfEnabled() { * edition which is not supported. */ @Test - public void testFailureIfCommunityUsedWithAnalytics() { + void testFailureIfCommunityUsedWithAnalytics() { try ( CouchbaseContainer container = new CouchbaseContainer(COUCHBASE_IMAGE_COMMUNITY) .withEnabledServices(CouchbaseService.KV, CouchbaseService.ANALYTICS) @@ -145,7 +136,7 @@ public void testFailureIfCommunityUsedWithAnalytics() { * edition which is not supported. */ @Test - public void testFailureIfCommunityUsedWithEventing() { + void testFailureIfCommunityUsedWithEventing() { try ( CouchbaseContainer container = new CouchbaseContainer(COUCHBASE_IMAGE_COMMUNITY) .withEnabledServices(CouchbaseService.KV, CouchbaseService.EVENTING) diff --git a/modules/cratedb/build.gradle b/modules/cratedb/build.gradle index 683b9c1a373..62405d559b2 100644 --- a/modules/cratedb/build.gradle +++ b/modules/cratedb/build.gradle @@ -1,10 +1,11 @@ description = "Testcontainers :: JDBC :: CrateDB" dependencies { - api project(':jdbc') + api project(':testcontainers-jdbc') - testImplementation project(':jdbc-test') - testRuntimeOnly 'org.postgresql:postgresql:42.7.1' + testRuntimeOnly 'org.postgresql:postgresql:42.7.12' - compileOnly 'org.jetbrains:annotations:24.1.0' + testImplementation project(':testcontainers-jdbc-test') + + compileOnly 'org.jetbrains:annotations:26.1.0' } diff --git a/modules/cratedb/src/main/java/org/testcontainers/cratedb/CrateDBContainer.java b/modules/cratedb/src/main/java/org/testcontainers/cratedb/CrateDBContainer.java index 039128174c1..d91704eba3d 100644 --- a/modules/cratedb/src/main/java/org/testcontainers/cratedb/CrateDBContainer.java +++ b/modules/cratedb/src/main/java/org/testcontainers/cratedb/CrateDBContainer.java @@ -47,7 +47,7 @@ public CrateDBContainer(final DockerImageName dockerImageName) { dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); withCommand("crate -C discovery.type=single-node"); - this.waitStrategy = Wait.forHttp("/").forPort(CRATEDB_HTTP_PORT).forStatusCode(200); + waitingFor(Wait.forHttp("/").forPort(CRATEDB_HTTP_PORT).forStatusCode(200)); addExposedPort(CRATEDB_PG_PORT); addExposedPort(CRATEDB_HTTP_PORT); diff --git a/modules/cratedb/src/test/java/org/testcontainers/jdbc/cratedb/CrateDBJDBCDriverTest.java b/modules/cratedb/src/test/java/org/testcontainers/jdbc/cratedb/CrateDBJDBCDriverTest.java index 54ef9704ac5..2c20afe6680 100644 --- a/modules/cratedb/src/test/java/org/testcontainers/jdbc/cratedb/CrateDBJDBCDriverTest.java +++ b/modules/cratedb/src/test/java/org/testcontainers/jdbc/cratedb/CrateDBJDBCDriverTest.java @@ -1,16 +1,12 @@ package org.testcontainers.jdbc.cratedb; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; -@RunWith(Parameterized.class) -public class CrateDBJDBCDriverTest extends AbstractJDBCDriverTest { +class CrateDBJDBCDriverTest extends AbstractJDBCDriverTest { - @Parameterized.Parameters(name = "{index} - {0}") public static Iterable data() { return Arrays.asList( new Object[][] { diff --git a/modules/cratedb/src/test/java/org/testcontainers/junit/cratedb/SimpleCrateDBTest.java b/modules/cratedb/src/test/java/org/testcontainers/junit/cratedb/SimpleCrateDBTest.java index 19ce83cd8ae..803b9173218 100644 --- a/modules/cratedb/src/test/java/org/testcontainers/junit/cratedb/SimpleCrateDBTest.java +++ b/modules/cratedb/src/test/java/org/testcontainers/junit/cratedb/SimpleCrateDBTest.java @@ -1,6 +1,6 @@ package org.testcontainers.junit.cratedb; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.CrateDBTestImages; import org.testcontainers.cratedb.CrateDBContainer; import org.testcontainers.db.AbstractContainerDatabaseTest; @@ -12,15 +12,18 @@ import static org.assertj.core.api.Assertions.assertThat; -public class SimpleCrateDBTest extends AbstractContainerDatabaseTest { +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 - public void testSimple() throws SQLException { - try (CrateDBContainer cratedb = new CrateDBContainer(CrateDBTestImages.CRATEDB_TEST_IMAGE)) { + void testSimple() throws SQLException { + try ( // container { + CrateDBContainer cratedb = new CrateDBContainer("crate:5.2.5") + // } + ) { cratedb.start(); ResultSet resultSet = performQuery(cratedb, "SELECT 1"); @@ -31,7 +34,7 @@ public void testSimple() throws SQLException { } @Test - public void testCommandOverride() throws SQLException { + void testCommandOverride() throws SQLException { try ( CrateDBContainer cratedb = new CrateDBContainer(CrateDBTestImages.CRATEDB_TEST_IMAGE) .withCommand("crate -C discovery.type=single-node -C cluster.name=testcontainers") @@ -40,12 +43,12 @@ public void testCommandOverride() throws SQLException { ResultSet resultSet = performQuery(cratedb, "select name from sys.cluster"); String result = resultSet.getString(1); - assertThat(result).as("cluster name should be overriden").isEqualTo("testcontainers"); + assertThat(result).as("cluster name should be overridden").isEqualTo("testcontainers"); } } @Test - public void testExplicitInitScript() throws SQLException { + void testExplicitInitScript() throws SQLException { try ( CrateDBContainer cratedb = new CrateDBContainer(CrateDBTestImages.CRATEDB_TEST_IMAGE) .withInitScript("somepath/init_cratedb.sql") diff --git a/modules/database-commons/build.gradle b/modules/database-commons/build.gradle index 9b125464596..345b354d739 100644 --- a/modules/database-commons/build.gradle +++ b/modules/database-commons/build.gradle @@ -2,6 +2,4 @@ description = "Testcontainers :: Database-Commons" dependencies { api project(':testcontainers') - - testImplementation 'org.assertj:assertj-core:3.25.1' } diff --git a/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptScanner.java b/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptScanner.java index 686527e9364..8350f7fbf38 100644 --- a/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptScanner.java +++ b/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptScanner.java @@ -141,7 +141,12 @@ Lexem next() { return Lexem.SEPARATOR; } else if (matchesSingleLineComment() || matchesMultilineComment()) { return Lexem.COMMENT; - } else if (matchesQuotedString('\'') || matchesQuotedString('"') || matchesDollarQuotedString()) { + } else if ( + matchesQuotedString('\'') || + matchesQuotedString('"') || + matchesQuotedString('`') || + matchesDollarQuotedString() + ) { return Lexem.QUOTED_STRING; } else if (matches(identifier)) { return Lexem.IDENTIFIER; diff --git a/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptSplitter.java b/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptSplitter.java index ba05b71e7bd..d71f6f3ac3d 100644 --- a/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptSplitter.java +++ b/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptSplitter.java @@ -36,7 +36,7 @@ void split() { //skip break; case WHITESPACE: - if (!sb.toString().endsWith(" ")) { + if (sb.length() == 0 || sb.charAt(sb.length() - 1) != ' ') { sb.append(' '); } break; diff --git a/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptUtils.java b/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptUtils.java index 419bcbf9e9a..b78ac4d63c4 100644 --- a/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptUtils.java +++ b/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptUtils.java @@ -27,6 +27,7 @@ import java.nio.charset.StandardCharsets; import java.util.LinkedList; import java.util.List; +import java.util.concurrent.TimeUnit; import javax.script.ScriptException; @@ -273,7 +274,7 @@ public static void executeDatabaseScript( LOGGER.info("Executing database script from " + scriptPath); } - long startTime = System.currentTimeMillis(); + long startTime = System.nanoTime(); List statements = new LinkedList<>(); if (separator == null) { @@ -306,7 +307,7 @@ public static void executeDatabaseScript( closeableDelegate.execute(statements, scriptPath, continueOnError, ignoreFailedDrops); } - long elapsedTime = System.currentTimeMillis() - startTime; + long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime); if (LOGGER.isInfoEnabled()) { LOGGER.info("Executed database script from " + scriptPath + " in " + elapsedTime + " ms."); } diff --git a/modules/database-commons/src/test/java/org/testcontainers/ext/ScriptScannerTest.java b/modules/database-commons/src/test/java/org/testcontainers/ext/ScriptScannerTest.java index 7e24026c4ec..a82305a0a6e 100644 --- a/modules/database-commons/src/test/java/org/testcontainers/ext/ScriptScannerTest.java +++ b/modules/database-commons/src/test/java/org/testcontainers/ext/ScriptScannerTest.java @@ -1,16 +1,16 @@ package org.testcontainers.ext; import org.apache.commons.lang3.StringUtils; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.regex.Pattern; import static org.assertj.core.api.Assertions.assertThat; -public class ScriptScannerTest { +class ScriptScannerTest { @Test - public void testHugeStringLiteral() { + void testHugeStringLiteral() { String script = "/* a comment */ \"" + StringUtils.repeat('~', 10000) + "\";"; ScriptScanner scanner = scanner(script); assertThat(scanner.next()).isEqualTo(ScriptScanner.Lexem.COMMENT); @@ -20,7 +20,7 @@ public void testHugeStringLiteral() { } @Test - public void testPgIdentifierWithDollarSigns() { + 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" @@ -32,7 +32,7 @@ public void testPgIdentifierWithDollarSigns() { } @Test - public void testQuotedLiterals() { + 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'"); diff --git a/modules/database-commons/src/test/java/org/testcontainers/ext/ScriptSplittingTest.java b/modules/database-commons/src/test/java/org/testcontainers/ext/ScriptSplittingTest.java index baaf3bbd08c..cb0e33162f4 100644 --- a/modules/database-commons/src/test/java/org/testcontainers/ext/ScriptSplittingTest.java +++ b/modules/database-commons/src/test/java/org/testcontainers/ext/ScriptSplittingTest.java @@ -1,6 +1,6 @@ package org.testcontainers.ext; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.Arrays; @@ -10,10 +10,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -public class ScriptSplittingTest { +class ScriptSplittingTest { @Test - public void testStringDemarcation() { + 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`'"); @@ -22,7 +22,7 @@ public void testStringDemarcation() { } @Test - public void testIssue1547Case1() { + void testIssue1547Case1() { String script = "create database if not exists ttt;\n" + "\n" + @@ -51,7 +51,7 @@ public void testIssue1547Case1() { } @Test - public void testIssue1547Case2() { + void testIssue1547Case2() { String script = "CREATE TABLE bar (\n" + " end_time VARCHAR(255)\n" + @@ -69,7 +69,16 @@ public void testIssue1547Case2() { } @Test - public void testUnusualSemicolonPlacement() { + 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"); @@ -78,7 +87,7 @@ public void testUnusualSemicolonPlacement() { } @Test - public void testCommentedSemicolon() { + void testCommentedSemicolon() { String script = "CREATE TABLE bar (\n" + " foo VARCHAR(255)\n" + "); \nDROP PROCEDURE IF EXISTS -- ;\n" + " count_foo"; @@ -91,7 +100,7 @@ public void testCommentedSemicolon() { } @Test - public void testStringEscaping() { + void testStringEscaping() { String script = "SELECT \"a /* string literal containing comment characters like -- here\";\n" + "SELECT \"a 'quoting' \\\"scenario ` involving BEGIN keyword\\\" here\";\n" + @@ -107,7 +116,7 @@ public void testStringEscaping() { } @Test - public void testBlockCommentExclusion() { + void testBlockCommentExclusion() { String script = "INSERT INTO bar (foo) /* ; */ VALUES ('hello world');"; List expected = Arrays.asList("INSERT INTO bar (foo) VALUES ('hello world')"); @@ -116,7 +125,7 @@ public void testBlockCommentExclusion() { } @Test - public void testBeginEndKeywordCorrectDetection() { + void testBeginEndKeywordCorrectDetection() { String script = "INSERT INTO something_end (begin_with_the_token, another_field) /*end*/ VALUES /* end */ (' begin ', `end`)-- begin\n;"; @@ -128,7 +137,7 @@ public void testBeginEndKeywordCorrectDetection() { } @Test - public void testCommentInStrings() { + void testCommentInStrings() { String script = "CREATE TABLE bar (foo VARCHAR(255));\n" + "\n" + @@ -152,7 +161,7 @@ public void testCommentInStrings() { } @Test - public void testMultipleBeginEndDetection() { + void testMultipleBeginEndDetection() { String script = "CREATE TABLE bar (foo VARCHAR(255));\n" + "\n" + @@ -196,7 +205,7 @@ public void testMultipleBeginEndDetection() { } @Test - public void testProcedureBlock() { + void testProcedureBlock() { String script = "CREATE PROCEDURE count_foo()\n" + " BEGIN\n" + @@ -255,7 +264,7 @@ public void testProcedureBlock() { } @Test - public void testUnclosedBlockComment() { + void testUnclosedBlockComment() { String script = "SELECT 'foo `bar`'; /*"; assertThatThrownBy(() -> doSplit(script, ScriptUtils.DEFAULT_STATEMENT_SEPARATOR)) .isInstanceOf(ScriptUtils.ScriptParseException.class) @@ -263,7 +272,7 @@ public void testUnclosedBlockComment() { } @Test - public void testIssue1452Case() { + void testIssue1452Case() { String script = "create table test (text VARCHAR(255));\n" + "\n" + @@ -279,7 +288,7 @@ public void testIssue1452Case() { } @Test - public void testIfLoopBlocks() { + void testIfLoopBlocks() { String script = "BEGIN\n" + " rec_loop: LOOP\n" + @@ -301,7 +310,7 @@ public void testIfLoopBlocks() { } @Test - public void testIfLoopBlocksSpecificSeparator() { + void testIfLoopBlocksSpecificSeparator() { String script = "BEGIN\n" + " rec_loop: LOOP\n" + @@ -327,14 +336,14 @@ public void testIfLoopBlocksSpecificSeparator() { } @Test - public void oracleStyleBlocks() { + void oracleStyleBlocks() { String script = "BEGIN END; /\n" + "BEGIN END;"; List expected = Arrays.asList("BEGIN END;", "BEGIN END;"); splitAndCompare(script, expected, "/"); } @Test - public void testMultiProcedureMySQLScript() { + void testMultiProcedureMySQLScript() { String script = "CREATE PROCEDURE doiterate(p1 INT)\n" + " BEGIN\n" + @@ -389,7 +398,7 @@ public void testMultiProcedureMySQLScript() { } @Test - public void testDollarQuotedStrings() { + void testDollarQuotedStrings() { String script = "CREATE FUNCTION f ()\n" + "RETURNS INT\n" + @@ -409,7 +418,7 @@ public void testDollarQuotedStrings() { } @Test - public void testNestedDollarQuotedString() { + 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" + @@ -430,7 +439,7 @@ public void testNestedDollarQuotedString() { } @Test - public void testUnclosedDollarQuotedString() { + void testUnclosedDollarQuotedString() { String script = "SELECT $tag$ ..... $"; assertThatThrownBy(() -> doSplit(script, ScriptUtils.DEFAULT_STATEMENT_SEPARATOR)) .isInstanceOf(ScriptUtils.ScriptParseException.class) @@ -461,12 +470,12 @@ private List doSplit(String script, String separator) { } @Test - public void testIgnoreDelimitersInLiteralsAndComments() { + void testIgnoreDelimitersInLiteralsAndComments() { assertThat(ScriptUtils.containsSqlScriptDelimiters("'@' /*@*/ \"@\" $tag$@$tag$ --@", "@")).isFalse(); } @Test - public void testContainsDelimiters() { + void testContainsDelimiters() { assertThat(ScriptUtils.containsSqlScriptDelimiters("'@' /*@*/ @ \"@\" --@", "@")).isTrue(); } } diff --git a/modules/databend/build.gradle b/modules/databend/build.gradle new file mode 100644 index 00000000000..7a779d3ca20 --- /dev/null +++ b/modules/databend/build.gradle @@ -0,0 +1,8 @@ +description = "Testcontainers :: JDBC :: Databend" + +dependencies { + api project(':testcontainers-jdbc') + + testImplementation project(':testcontainers-jdbc-test') + testRuntimeOnly 'com.databend:databend-jdbc:0.4.6' +} diff --git a/modules/databend/src/main/java/org/testcontainers/databend/DatabendContainer.java b/modules/databend/src/main/java/org/testcontainers/databend/DatabendContainer.java new file mode 100644 index 00000000000..389c8c44963 --- /dev/null +++ b/modules/databend/src/main/java/org/testcontainers/databend/DatabendContainer.java @@ -0,0 +1,112 @@ +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; + } +} diff --git a/modules/databend/src/main/java/org/testcontainers/databend/DatabendContainerProvider.java b/modules/databend/src/main/java/org/testcontainers/databend/DatabendContainerProvider.java new file mode 100644 index 00000000000..9142bebcfd3 --- /dev/null +++ b/modules/databend/src/main/java/org/testcontainers/databend/DatabendContainerProvider.java @@ -0,0 +1,28 @@ +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(); + } + } +} diff --git a/modules/databend/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider b/modules/databend/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider new file mode 100644 index 00000000000..ead69e77bee --- /dev/null +++ b/modules/databend/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider @@ -0,0 +1 @@ +org.testcontainers.databend.DatabendContainerProvider diff --git a/modules/databend/src/test/java/org/testcontainers/databend/DatabendContainerTest.java b/modules/databend/src/test/java/org/testcontainers/databend/DatabendContainerTest.java new file mode 100644 index 00000000000..984f42979c4 --- /dev/null +++ b/modules/databend/src/test/java/org/testcontainers/databend/DatabendContainerTest.java @@ -0,0 +1,44 @@ +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); + } + } +} diff --git a/modules/databend/src/test/java/org/testcontainers/databend/DatabendJDBCDriverTest.java b/modules/databend/src/test/java/org/testcontainers/databend/DatabendJDBCDriverTest.java new file mode 100644 index 00000000000..2f3a030919f --- /dev/null +++ b/modules/databend/src/test/java/org/testcontainers/databend/DatabendJDBCDriverTest.java @@ -0,0 +1,17 @@ +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) }, + } + ); + } +} diff --git a/modules/databend/src/test/resources/logback-test.xml b/modules/databend/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..83ef7a1a3ef --- /dev/null +++ b/modules/databend/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + diff --git a/modules/db2/build.gradle b/modules/db2/build.gradle index 0d5532dd36c..afae0bfd751 100644 --- a/modules/db2/build.gradle +++ b/modules/db2/build.gradle @@ -1,9 +1,8 @@ description = "Testcontainers :: JDBC :: DB2" dependencies { - api project(':jdbc') + api project(':testcontainers-jdbc') - testImplementation project(':jdbc-test') - testRuntimeOnly 'com.ibm.db2:jcc:11.5.9.0' - testImplementation 'org.assertj:assertj-core:3.25.1' + testImplementation project(':testcontainers-jdbc-test') + testRuntimeOnly 'com.ibm.db2:jcc:12.1.5.0' } diff --git a/modules/db2/src/main/java/org/testcontainers/containers/Db2Container.java b/modules/db2/src/main/java/org/testcontainers/containers/Db2Container.java index 0cde8f0dee3..19dbc6a58ac 100644 --- a/modules/db2/src/main/java/org/testcontainers/containers/Db2Container.java +++ b/modules/db2/src/main/java/org/testcontainers/containers/Db2Container.java @@ -1,5 +1,6 @@ package org.testcontainers.containers; +import com.github.dockerjava.api.model.Capability; import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.LicenseAcceptance; @@ -17,7 +18,9 @@ *
    *
  • Database: 50000
  • *
+ * @deprecated use {@link org.testcontainers.db2.Db2Container} instead. */ +@Deprecated public class Db2Container extends JdbcDatabaseContainer { public static final String NAME = "db2"; @@ -57,7 +60,7 @@ public Db2Container(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_NEW_IMAGE_NAME, DEFAULT_IMAGE_NAME); - withPrivilegedMode(true); + withCreateContainerCmdModifier(cmd -> cmd.withCapAdd(Capability.IPC_LOCK).withCapAdd(Capability.IPC_OWNER)); this.waitStrategy = new LogMessageWaitStrategy() .withRegEx(".*Setup has completed\\..*") @@ -78,7 +81,7 @@ protected Set getLivenessCheckPorts() { @Override protected void configure() { - // If license was not accepted programatically, check if it was accepted via resource file + // If license was not accepted programmatically, check if it was accepted via resource file if (!getEnvMap().containsKey("LICENSE")) { LicenseAcceptance.assertLicenseAccepted(this.getDockerImageName()); acceptLicense(); diff --git a/modules/db2/src/main/java/org/testcontainers/db2/Db2Container.java b/modules/db2/src/main/java/org/testcontainers/db2/Db2Container.java new file mode 100644 index 00000000000..bdd182599e5 --- /dev/null +++ b/modules/db2/src/main/java/org/testcontainers/db2/Db2Container.java @@ -0,0 +1,143 @@ +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"; + } +} diff --git a/modules/db2/src/test/java/org/testcontainers/Db2TestImages.java b/modules/db2/src/test/java/org/testcontainers/Db2TestImages.java index b0bfc869272..002d030b1c5 100644 --- a/modules/db2/src/test/java/org/testcontainers/Db2TestImages.java +++ b/modules/db2/src/test/java/org/testcontainers/Db2TestImages.java @@ -3,5 +3,5 @@ import org.testcontainers.utility.DockerImageName; public interface Db2TestImages { - DockerImageName DB2_IMAGE = DockerImageName.parse("ibmcom/db2:11.5.0.0a"); + DockerImageName DB2_IMAGE = DockerImageName.parse("icr.io/db2_community/db2:11.5.8.0"); } diff --git a/modules/db2/src/test/java/org/testcontainers/junit/db2/SimpleDb2Test.java b/modules/db2/src/test/java/org/testcontainers/db2/Db2ContainerTest.java similarity index 78% rename from modules/db2/src/test/java/org/testcontainers/junit/db2/SimpleDb2Test.java rename to modules/db2/src/test/java/org/testcontainers/db2/Db2ContainerTest.java index 2cffbdb117a..6b17d1583f7 100644 --- a/modules/db2/src/test/java/org/testcontainers/junit/db2/SimpleDb2Test.java +++ b/modules/db2/src/test/java/org/testcontainers/db2/Db2ContainerTest.java @@ -1,8 +1,7 @@ -package org.testcontainers.junit.db2; +package org.testcontainers.db2; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.Db2TestImages; -import org.testcontainers.containers.Db2Container; import org.testcontainers.db.AbstractContainerDatabaseTest; import java.sql.ResultSet; @@ -10,11 +9,14 @@ import static org.assertj.core.api.Assertions.assertThat; -public class SimpleDb2Test extends AbstractContainerDatabaseTest { +class Db2ContainerTest extends AbstractContainerDatabaseTest { @Test - public void testSimple() throws SQLException { - try (Db2Container db2 = new Db2Container(Db2TestImages.DB2_IMAGE).acceptLicense()) { + 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"); @@ -26,7 +28,7 @@ public void testSimple() throws SQLException { } @Test - public void testSimpleWithNewImage() throws SQLException { + void testSimpleWithNewImage() throws SQLException { try (Db2Container db2 = new Db2Container("icr.io/db2_community/db2:11.5.8.0").acceptLicense()) { db2.start(); @@ -39,7 +41,7 @@ public void testSimpleWithNewImage() throws SQLException { } @Test - public void testWithAdditionalUrlParamInJdbcUrl() { + void testWithAdditionalUrlParamInJdbcUrl() { try ( Db2Container db2 = new Db2Container(Db2TestImages.DB2_IMAGE) .withUrlParam("sslConnection", "false") diff --git a/modules/db2/src/test/java/org/testcontainers/jdbc/db2/DB2JDBCDriverTest.java b/modules/db2/src/test/java/org/testcontainers/jdbc/db2/DB2JDBCDriverTest.java index 4a03f824a28..cfa74c8017a 100644 --- a/modules/db2/src/test/java/org/testcontainers/jdbc/db2/DB2JDBCDriverTest.java +++ b/modules/db2/src/test/java/org/testcontainers/jdbc/db2/DB2JDBCDriverTest.java @@ -1,16 +1,12 @@ package org.testcontainers.jdbc.db2; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; -@RunWith(Parameterized.class) -public class DB2JDBCDriverTest extends AbstractJDBCDriverTest { +class DB2JDBCDriverTest extends AbstractJDBCDriverTest { - @Parameterized.Parameters(name = "{index} - {0}") public static Iterable data() { return Arrays.asList( new Object[][] { // diff --git a/modules/dynalite/build.gradle b/modules/dynalite/build.gradle deleted file mode 100644 index be947cc349e..00000000000 --- a/modules/dynalite/build.gradle +++ /dev/null @@ -1,9 +0,0 @@ -description = "Testcontainers :: Dynalite (deprecated)" - -dependencies { - api project(':testcontainers') - - compileOnly 'com.amazonaws:aws-java-sdk-dynamodb:1.12.643' - testImplementation 'com.amazonaws:aws-java-sdk-dynamodb:1.12.643' - testImplementation 'org.assertj:assertj-core:3.25.2' -} diff --git a/modules/dynalite/src/main/java/org/testcontainers/dynamodb/DynaliteContainer.java b/modules/dynalite/src/main/java/org/testcontainers/dynamodb/DynaliteContainer.java deleted file mode 100644 index 2a73ce6f022..00000000000 --- a/modules/dynalite/src/main/java/org/testcontainers/dynamodb/DynaliteContainer.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.testcontainers.dynamodb; - -import com.amazonaws.auth.AWSCredentialsProvider; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.client.builder.AwsClientBuilder; -import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; -import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.utility.DockerImageName; - -/** - * Container for Dynalite, a DynamoDB clone. - * - * @deprecated use localstack module instead - */ -public class DynaliteContainer extends GenericContainer { - - private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("quay.io/testcontainers/dynalite"); - - private static final String DEFAULT_TAG = "v1.2.1-1"; - - private static final int MAPPED_PORT = 4567; - - /** - * @deprecated use {@link #DynaliteContainer(DockerImageName)} instead - */ - @Deprecated - public DynaliteContainer() { - this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); - } - - public DynaliteContainer(String dockerImageName) { - this(DockerImageName.parse(dockerImageName)); - } - - public DynaliteContainer(final DockerImageName dockerImageName) { - super(dockerImageName); - dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); - - withExposedPorts(MAPPED_PORT); - } - - /** - * Gets a preconfigured {@link AmazonDynamoDB} client object for connecting to this - * container. - * - * @return preconfigured client - */ - public AmazonDynamoDB getClient() { - return AmazonDynamoDBClientBuilder - .standard() - .withEndpointConfiguration(getEndpointConfiguration()) - .withCredentials(getCredentials()) - .build(); - } - - /** - * Gets {@link AwsClientBuilder.EndpointConfiguration} - * that may be used to connect to this container. - * - * @return endpoint configuration - */ - public AwsClientBuilder.EndpointConfiguration getEndpointConfiguration() { - return new AwsClientBuilder.EndpointConfiguration( - "http://" + this.getHost() + ":" + this.getMappedPort(MAPPED_PORT), - null - ); - } - - /** - * Gets an {@link AWSCredentialsProvider} that may be used to connect to this container. - * - * @return dummy AWS credentials - */ - public AWSCredentialsProvider getCredentials() { - return new AWSStaticCredentialsProvider(new BasicAWSCredentials("dummy", "dummy")); - } -} diff --git a/modules/dynalite/src/test/java/org/testcontainers/dynamodb/DynaliteContainerTest.java b/modules/dynalite/src/test/java/org/testcontainers/dynamodb/DynaliteContainerTest.java deleted file mode 100644 index 10e02566d16..00000000000 --- a/modules/dynalite/src/test/java/org/testcontainers/dynamodb/DynaliteContainerTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.testcontainers.dynamodb; - -import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; -import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder; -import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; -import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; -import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; -import com.amazonaws.services.dynamodbv2.model.KeyType; -import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; -import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType; -import com.amazonaws.services.dynamodbv2.model.TableDescription; -import org.junit.Rule; -import org.junit.Test; -import org.testcontainers.utility.DockerImageName; - -import static org.assertj.core.api.Assertions.assertThat; - -public class DynaliteContainerTest { - - private static final DockerImageName DYNALITE_IMAGE = DockerImageName.parse( - "quay.io/testcontainers/dynalite:v1.2.1-1" - ); - - @Rule - public DynaliteContainer dynamoDB = new DynaliteContainer(DYNALITE_IMAGE); - - @Test - public void simpleTestWithManualClientCreation() { - final AmazonDynamoDB client = AmazonDynamoDBClientBuilder - .standard() - .withEndpointConfiguration(dynamoDB.getEndpointConfiguration()) - .withCredentials(dynamoDB.getCredentials()) - .build(); - - runTest(client); - } - - @Test - public void simpleTestWithProvidedClient() { - final AmazonDynamoDB client = dynamoDB.getClient(); - - runTest(client); - } - - private void runTest(AmazonDynamoDB client) { - CreateTableRequest request = new CreateTableRequest() - .withAttributeDefinitions(new AttributeDefinition("Name", ScalarAttributeType.S)) - .withKeySchema(new KeySchemaElement("Name", KeyType.HASH)) - .withProvisionedThroughput(new ProvisionedThroughput(10L, 10L)) - .withTableName("foo"); - - client.createTable(request); - - final TableDescription tableDescription = client.describeTable("foo").getTable(); - - assertThat(tableDescription).as("the description is not null").isNotNull(); - assertThat(tableDescription.getTableName()).as("the table has the right name").isEqualTo("foo"); - assertThat(tableDescription.getKeySchema().get(0).getAttributeName()) - .as("the name has the right primary key") - .isEqualTo("Name"); - } -} diff --git a/modules/elasticsearch/build.gradle b/modules/elasticsearch/build.gradle index 56399a509d9..7f57916e991 100644 --- a/modules/elasticsearch/build.gradle +++ b/modules/elasticsearch/build.gradle @@ -2,7 +2,7 @@ description = "Testcontainers :: elasticsearch" dependencies { api project(':testcontainers') - testImplementation "org.elasticsearch.client:elasticsearch-rest-client:8.12.0" - testImplementation "org.elasticsearch.client:transport:7.17.17" - testImplementation 'org.assertj:assertj-core:3.25.2' + + testImplementation "org.elasticsearch.client:elasticsearch-rest-client:9.4.3" + testImplementation "org.elasticsearch.client:transport:7.17.29" } diff --git a/modules/elasticsearch/src/main/java/org/testcontainers/elasticsearch/ElasticsearchContainer.java b/modules/elasticsearch/src/main/java/org/testcontainers/elasticsearch/ElasticsearchContainer.java index 940d6924c5e..2080d21649a 100644 --- a/modules/elasticsearch/src/main/java/org/testcontainers/elasticsearch/ElasticsearchContainer.java +++ b/modules/elasticsearch/src/main/java/org/testcontainers/elasticsearch/ElasticsearchContainer.java @@ -1,13 +1,12 @@ package org.testcontainers.elasticsearch; -import com.github.dockerjava.api.command.InspectContainerResponse; 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.LogMessageWaitStrategy; +import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.ComparableVersion; import org.testcontainers.utility.DockerImageName; @@ -66,31 +65,19 @@ public class ElasticsearchContainer extends GenericContainer= 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 Optional caCertAsBytes = Optional.empty(); - - private String certPath = "/usr/share/elasticsearch/config/certs/http_ca.crt"; - - /** - * @deprecated use {@link #ElasticsearchContainer(DockerImageName)} instead - */ - @Deprecated - public ElasticsearchContainer() { - this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); - } + 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) { @@ -99,6 +86,7 @@ public ElasticsearchContainer(String 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) { @@ -133,26 +121,10 @@ public ElasticsearchContainer(final DockerImageName dockerImageName) { // 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(new LogMessageWaitStrategy().withRegEx(regex)); + setWaitStrategy(Wait.forLogMessage(regex, 1)); if (isAtLeastMajorVersion8) { withPassword(ELASTICSEARCH_DEFAULT_PASSWORD); - } - } - - @Override - protected void containerIsStarted(InspectContainerResponse containerInfo) { - if (isAtLeastMajorVersion8 && StringUtils.isNotEmpty(certPath)) { - try { - byte[] bytes = copyFileFromContainer(certPath, IOUtils::toByteArray); - if (bytes.length > 0) { - this.caCertAsBytes = 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."); - } + withCertPath(DEFAULT_CERT_PATH); } } @@ -162,17 +134,36 @@ protected void containerIsStarted(InspectContainerResponse containerInfo) { * @return byte array optional containing the CA cert extracted from the docker container */ public Optional caCertAsBytes() { - return 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 + * 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.get())); + 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); @@ -190,13 +181,13 @@ public SSLContext createSslContextFromCa() { /** * 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 + * @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" + "You can not activate security on Elastic OSS Image. Please switch to the default distribution" ); } withEnv("ELASTIC_PASSWORD", password); @@ -218,11 +209,101 @@ public ElasticsearchContainer withCertPath(String certPath) { return this; } + String getCertPath() { + return certPath; + } + public String getHttpHostAddress() { return getHost() + ":" + getMappedPort(ELASTICSEARCH_DEFAULT_PORT); } - @Deprecated // The TransportClient will be removed in Elasticsearch 8. No need to expose this port anymore in the future. + /** + * Checks env first if this implies HTTP/HTTPS. + * Otherwise, detects the scheme used by Elasticsearch using curl + * + * @return "http" or "https" + */ + String getHttpScheme() { + String securityEnabled = getEnvMap().get("xpack.security.enabled"); + String httpSslEnabled = getEnvMap().get("xpack.security.http.ssl.enabled"); + + // Respect explicit user config + if ("false".equalsIgnoreCase(securityEnabled) || "false".equalsIgnoreCase(httpSslEnabled)) { + return "http"; + } + if ("true".equalsIgnoreCase(httpSslEnabled)) { + return "https"; + } + + if (!isRunning()) { + throw new IllegalStateException( + "Cannot determine HTTP scheme: environment variables are not set and container is not running for curl probe" + ); + } + + ExecResult httpsResult = null; + ExecResult httpResult = null; + try { + // HTTPS probe: any HTTP response (200/401/403/...) => scheme is HTTPS. + // http_code == 000 means we didn't get an HTTP response (TLS/connect failure/timeout). + httpsResult = + execInContainer( + "curl", + "-sS", + "-k", + "--connect-timeout", + "2", + "--max-time", + "4", + "-o", + "/dev/null", + "-w", + "%{http_code}", + "https://localhost:" + ELASTICSEARCH_DEFAULT_PORT + "/" + ); + if (httpsResult.getExitCode() == 0 && !"000".equals(httpsResult.getStdout().trim())) { + return "https"; + } + + // HTTP probe + httpResult = + execInContainer( + "curl", + "-sS", + "--connect-timeout", + "2", + "--max-time", + "4", + "-o", + "/dev/null", + "-w", + "%{http_code}", + "http://localhost:" + ELASTICSEARCH_DEFAULT_PORT + "/" + ); + if (httpResult.getExitCode() == 0 && !"000".equals(httpResult.getStdout().trim())) { + return "http"; + } + } catch (Exception e) { + throw new RuntimeException("Failed to detect protocol via curl", e); + } + + throw new RuntimeException( + String.format( + "Failed to detect protocol via curl. Both HTTPS and HTTP probes failed. " + + "HTTPS probe - exit code: %d, stdout: %s, stderr: %s; " + + "HTTP probe - exit code: %d, stdout: %s, stderr: %s", + httpsResult.getExitCode(), + httpsResult.getStdout(), + httpsResult.getStderr(), + httpResult.getExitCode(), + httpResult.getStdout(), + httpResult.getStderr() + ) + ); + } + + // 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)); } diff --git a/modules/elasticsearch/src/main/java/org/testcontainers/elasticsearch/KibanaContainer.java b/modules/elasticsearch/src/main/java/org/testcontainers/elasticsearch/KibanaContainer.java new file mode 100644 index 00000000000..ee8ce714854 --- /dev/null +++ b/modules/elasticsearch/src/main/java/org/testcontainers/elasticsearch/KibanaContainer.java @@ -0,0 +1,669 @@ +package org.testcontainers.elasticsearch; + +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 org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.rnorth.ducttape.unreliables.Unreliables; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.DockerClientFactory; +import org.testcontainers.containers.Container; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.builder.Transferable; +import org.testcontainers.utility.Base58; +import org.testcontainers.utility.ComparableVersion; +import org.testcontainers.utility.DockerImageName; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * Testcontainers implementation for Kibana. + * Minimum supported version: {@value MINIMUM_SUPPORTED_VERSION} + *

+ * Supports two modes: + *

    + *
  • Managed mode: Kibana automatically connects to an {@link ElasticsearchContainer}. + * See KibanaContainerTest#managedModeCanStartAndReachElasticsearchInSameExplicitNetwork()
  • + *
  • External mode: Kibana connects to an external Elasticsearch instance via URL. + * See KibanaContainerTest#externalModeCanWorkWithUsernamePassword()
  • + *
+ *

+ */ +public class KibanaContainer extends GenericContainer { + + private static final String ES_CA_CERT_PATH = "/usr/share/kibana/config/certs/es-ca.crt"; + + private static final Logger log = LoggerFactory.getLogger(KibanaContainer.class); + + private static final int KIBANA_DEFAULT_PORT = 5601; + + private static final String KIBANA_SYSTEM_USER = "kibana_system"; + + private static final String MINIMUM_SUPPORTED_VERSION = "8.0.0"; + + private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("docker.elastic.co/kibana/kibana"); + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private String encryptionKey; + + private ElasticsearchContainer elasticsearch; + + private String elasticsearchUrl; + + private String elasticsearchUsername; + + private String elasticsearchPassword; + + private String elasticsearchServiceAccountToken; + + private byte[] elasticsearchCaCertificate; + + private Duration startupTimeout = Duration.ofSeconds(120); + + /** + * If KibanaContainer creates an ad-hoc shared network (managed mode, neither container has an explicit network), + * it owns closing it. + */ + private Network createdSharedNetwork; + + /** + * Creates a KibanaContainer in managed mode. + * Kibana automatically connects to the provided Elasticsearch container. + * + * @param elasticsearch the Elasticsearch container to connect to + */ + public KibanaContainer(ElasticsearchContainer elasticsearch) { + this(buildDockerImageName(elasticsearch)); + this.elasticsearch = elasticsearch; + dependsOn(elasticsearch); + } + + /** + * Creates a KibanaContainer in external mode. + * Use {@link #withElasticsearchUrl(String)} to configure the Elasticsearch connection. Use other methods to provide security credentials and such. + * + * @param dockerImageName the Docker image name + */ + public KibanaContainer(String dockerImageName) { + this(DockerImageName.parse(dockerImageName)); + } + + /** + * Creates a KibanaContainer in external mode. + * Use {@link #withElasticsearchUrl(String)} to configure the Elasticsearch connection. Use other methods to provide security credentials and such. + * + * @param dockerImageName the Docker image name + */ + public KibanaContainer(final DockerImageName dockerImageName) { + super(dockerImageName); + ensureCompatibleVersion(dockerImageName.getVersionPart()); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + this.encryptionKey = stableConfigKey(dockerImageName); + + withExposedPorts(KIBANA_DEFAULT_PORT); + //we have to explicitly set wait the strategy later on in configure, once we know the security configuration + setWaitStrategy(null); + } + + /** + * Sets the encryption key used for Kibana's encrypted saved objects. + * The key must be at least 32 characters. When not set, a deterministic default derived from + * the image name is used, which is required for {@link #withReuse(boolean)} to work correctly. + * + * @param encryptionKey the encryption key + * @return this container instance + */ + public KibanaContainer withEncryptionKey(String encryptionKey) { + if (encryptionKey == null || encryptionKey.length() < 32) { + throw new IllegalArgumentException("Kibana encryption key must be at least 32 characters long"); + } + this.encryptionKey = encryptionKey; + return this; + } + + /** + * Enables or disables container reuse across JVM runs. + * + *

Supported in external mode only. When Kibana is configured via + * {@link #withElasticsearchUrl(String)}, the container configuration is fully deterministic + * and TC can reliably locate the running container on subsequent runs. + * + *

Reuse is not supported in managed mode (i.e. when this container was created with + * an {@link ElasticsearchContainer}). Managed mode introduces several non-deterministic inputs + * into the container hash on every run (ad-hoc network ID, random network alias, fresh service + * account token), so TC always sees a different hash and starts a fresh container instead of + * reusing the existing one. This is a framework-level characteristic that affects any container + * connected via {@code withNetwork()} — not specific to {@code KibanaContainer}. + * + * @param reusable whether to enable container reuse + * @return this container instance + * @throws IllegalStateException if {@code reusable} is {@code true} and managed mode is active + */ + @Override + public KibanaContainer withReuse(boolean reusable) { + if (reusable && elasticsearch != null) { + throw new IllegalStateException( + "withReuse(true) is not supported for KibanaContainer in managed mode. " + + "Use external mode (withElasticsearchUrl) to enable reuse." + ); + } + return super.withReuse(reusable); + } + + /** + * Configures the Elasticsearch URL for external mode. + * + * @param elasticsearchUrl the Elasticsearch URL (e.g., "https://my.fancy.setup.elastic.cloud:9200") + * @return this container instance + * @throws IllegalStateException if already using managed mode + */ + public KibanaContainer withElasticsearchUrl(String elasticsearchUrl) { + if (elasticsearch != null) { + throw new IllegalStateException("Cannot set Elasticsearch URL when using Elasticsearch container"); + } + this.elasticsearchUrl = elasticsearchUrl; + return this; + } + + /** + * Configures Kibana to authenticate using the kibana_system user. + * + * @param password the password for the kibana_system user + * @return this container instance + */ + public KibanaContainer withKibanaSystemPassword(String password) { + return withKibanaUsernameAndPassword(KIBANA_SYSTEM_USER, password); + } + + /** + * Configures credentials Kibana will use for authentication. + * + * @param username the Elasticsearch username (cannot be 'elastic') + * @param password the password + * @return this container instance + * @throws IllegalStateException if a service account token is already configured + * @throws IllegalArgumentException if credentials are invalid + */ + public KibanaContainer withKibanaUsernameAndPassword(String username, String password) { + if (elasticsearchServiceAccountToken != null) { + throw new IllegalStateException( + "Conflicting Elasticsearch credentials: provide either a service account token " + + "or a username/password pair, not both." + ); + } + if (StringUtils.isAnyBlank(username, password)) { + throw new IllegalArgumentException("Kibana credentials cannot be blank"); + } + if (!username.equals(username.trim()) || !password.equals(password.trim())) { + throw new IllegalArgumentException("Kibana credentials cannot have leading or trailing whitespace"); + } + if ("elastic".equals(username)) { + throw new IllegalArgumentException("Username 'elastic' is reserved for internal use by Elasticsearch"); + } + + this.elasticsearchUsername = username; + this.elasticsearchPassword = password; + return this; + } + + /** + * Configures a service account token for Elasticsearch authentication. + * + * @param token the service account token + * @return this container instance + * @throws IllegalStateException if username/password credentials are already configured + * @throws IllegalArgumentException if token is blank + */ + public KibanaContainer withElasticsearchServiceAccountToken(String token) { + if (elasticsearchUsername != null) { + throw new IllegalStateException( + "Conflicting Elasticsearch credentials: provide either a service account token " + + "or a username/password pair, not both." + ); + } + if (StringUtils.isBlank(token)) { + throw new IllegalArgumentException("Service account token cannot be empty"); + } + + if (!token.equals(token.trim())) { + throw new IllegalArgumentException("Service token cannot have leading or trailing whitespace"); + } + this.elasticsearchServiceAccountToken = token; + return this; + } + + /** + * Configures the Elasticsearch CA certificate for HTTPS connections. + * + * @param caCertificate the CA certificate in PEM format + * @return this container instance + * @throws IllegalArgumentException if certificate is empty + */ + public KibanaContainer withElasticsearchCaCertificate(byte[] caCertificate) { + if (caCertificate == null || caCertificate.length == 0) { + throw new IllegalArgumentException("Elasticsearch CA certificate cannot be empty"); + } + this.elasticsearchCaCertificate = caCertificate; + return this; + } + + @Override + protected void configure() { + super.configure(); + + addEnv("XPACK_ENCRYPTEDSAVEDOBJECTS_ENCRYPTIONKEY", encryptionKey); + addEnv("SERVER_NAME", "kibana"); + + if (elasticsearchCaCertificate != null) { + withCopyToContainer(Transferable.of(elasticsearchCaCertificate), ES_CA_CERT_PATH); + addEnv("ELASTICSEARCH_SSL_CERTIFICATEAUTHORITIES", ES_CA_CERT_PATH); + } + if (elasticsearch != null) { + configureManagedElasticsearch(); + } else if (elasticsearchUrl != null) { + configureExternalElasticsearch(); + } else { + throw new IllegalStateException( + "Elasticsearch must be configured either via constructor KibanaContainer(elasticsearch) " + + "or via .withElasticsearchUrl() for external Elasticsearch" + ); + } + //wait strategy is set in configure, because we don't know the security configuration before + configureWaitStrategy(); + } + + @Override + protected void containerIsStarted(InspectContainerResponse containerInfo) { + super.containerIsStarted(containerInfo); + log.info("Kibana is now ready, it can be accessed at http://{}", getHttpHostAddress()); + } + + @Override + public void stop() { + super.stop(); + if (createdSharedNetwork != null) { + try { + createdSharedNetwork.close(); + } catch (Exception e) { + log.debug("Failed to close shared network", e); + } finally { + createdSharedNetwork = null; + } + } + } + + @Override + public KibanaContainer withStartupTimeout(Duration startupTimeout) { + this.startupTimeout = startupTimeout; + return this; + } + + private static DockerImageName buildDockerImageName(ElasticsearchContainer elasticsearch) { + String esVersion = DockerImageName.parse(elasticsearch.getDockerImageName()).getVersionPart(); + ensureCompatibleVersion(esVersion); + return DEFAULT_IMAGE_NAME.withTag(esVersion); + } + + private void configureExternalElasticsearch() { + addEnv("ELASTICSEARCH_HOSTS", elasticsearchUrl); + if (elasticsearchServiceAccountToken != null) { + addEnv("ELASTICSEARCH_SERVICEACCOUNTTOKEN", elasticsearchServiceAccountToken); + } else if (elasticsearchUsername != null && elasticsearchPassword != null) { + addEnv("ELASTICSEARCH_USERNAME", elasticsearchUsername); + addEnv("ELASTICSEARCH_PASSWORD", elasticsearchPassword); + } else { + log.info( + "No Elasticsearch credentials provided for external mode; Kibana will attempt to connect anonymously" + ); + } + } + + private void configureManagedElasticsearch() { + ensureCorrectNetworkSetupForManagedMode(); + + if (getNetwork() == null) { + createAdHocNetwork(); + } + + String protocol = elasticsearch.getHttpScheme(); + + String hosts = protocol + "://" + resolveExistingEsDnsNameOnNetwork(getNetwork()) + ":9200"; + addEnv("ELASTICSEARCH_HOSTS", hosts); + + if ("https".equals(protocol)) { + // In managed mode, if Elasticsearch uses HTTPS we must configure Kibana with the ES CA, unless provided by user + if (this.elasticsearchCaCertificate == null) { + byte[] ca = copyElasticsearchHttpCaCertificateOrThrow(); + + withCopyToContainer(Transferable.of(ca), ES_CA_CERT_PATH); + addEnv("ELASTICSEARCH_SSL_CERTIFICATEAUTHORITIES", ES_CA_CERT_PATH); + } + if (elasticsearch != null && !getEnvMap().containsKey("ELASTICSEARCH_SSL_VERIFICATIONMODE")) { + addEnv("ELASTICSEARCH_SSL_VERIFICATIONMODE", "certificate"); + } + } + + // Elasticsearch 8.x+ has the security enabled by default, so lack of the env var set to false means security is enabled + boolean securityDisabled = "false".equalsIgnoreCase(elasticsearch.getEnvMap().get("xpack.security.enabled")); + + if (!securityDisabled) { + // Managed mode: authenticate Kibana -> Elasticsearch using a Kibana service account token. + // This avoids any password lifecycle management for kibana_system. + String token = createKibanaServiceAccountToken(protocol); + addEnv("ELASTICSEARCH_SERVICEACCOUNTTOKEN", token); + } + } + + private byte[] copyElasticsearchHttpCaCertificateOrThrow() { + try { + return elasticsearch.copyFileFromContainer(elasticsearch.getCertPath(), IOUtils::toByteArray); + } catch (Exception e) { + throw new IllegalStateException( + "Failed to copy Elasticsearch HTTP CA certificate from '" + + elasticsearch.getCertPath() + + "'. " + + "In managed HTTPS mode, KibanaContainer requires access to the Elasticsearch HTTP CA.", + e + ); + } + } + + private void ensureCorrectNetworkSetupForManagedMode() { + Network esNetwork = elasticsearch.getNetwork(); + Network kbNetwork = this.getNetwork(); + + if ((esNetwork == null) != (kbNetwork == null)) { + throw new IllegalStateException( + "Managed mode requires either both containers share the same explicit network, " + + "or neither specifies a network (KibanaContainer will create one). " + ); + } + + // Both explicit: must be same + if (esNetwork != kbNetwork) { + throw new IllegalStateException( + "Elasticsearch and Kibana have different networks configured. " + + "In managed mode both containers must share the same explicit network instance, " + + "or neither must define a network." + ); + } + } + + private void createAdHocNetwork() { + // Fully managed: create ad-hoc network and own it. + createdSharedNetwork = Network.newNetwork(); + withNetwork(createdSharedNetwork); + + // Managed-mode safety rule: by the time Kibana is configuring itself, Elasticsearch must already be + // started (via dependsOn) + String esId = requireElasticsearchContainerId(); + + // Elasticsearch is already created/started. Attach it to the ad-hoc network. + // We don't need to provide an explicit alias - we'll use the container name for DNS resolution. + // Equivalent of https://docs.docker.com/reference/cli/docker/network/connect/ + connectRunningContainerToNetwork(esId, createdSharedNetwork); + } + + private String resolveExistingEsDnsNameOnNetwork(Network network) { + String esId = requireElasticsearchContainerId(); + + InspectContainerResponse info = DockerClientFactory.instance().client().inspectContainerCmd(esId).exec(); + + Map networks = info.getNetworkSettings().getNetworks(); + if (networks == null) { + throw new IllegalStateException("Elasticsearch container has no network configuration"); + } + + // Try to find the network endpoint - Docker may key by network name or ID + ContainerNetwork endpoint = findNetworkEndpoint(networks, network); + if (endpoint == null) { + throw new IllegalStateException( + "Elasticsearch container is not connected to the expected network. " + + "Ensure both containers use the same Network instance." + ); + } + + // Prefer user-defined network aliases (skip Testcontainers auto-generated tc-* aliases) + if (endpoint.getAliases() != null && !endpoint.getAliases().isEmpty()) { + for (String alias : endpoint.getAliases()) { + if (StringUtils.isNotBlank(alias)) { + String cleaned = alias.trim(); + // Skip Testcontainers auto-generated aliases (tc-*), prefer user-defined ones + if (!cleaned.startsWith("tc-")) { + log.info("Using Elasticsearch network alias: {}", cleaned); + return cleaned; + } + } + } + } + + // Fallback: use container name + String containerName = info.getName(); + if (containerName != null && !containerName.trim().isEmpty()) { + String dnsName = containerName.replaceFirst("^/", "").trim(); + log.info("No user-defined network alias found, using Elasticsearch container name: {}", dnsName); + return dnsName; + } + + throw new IllegalStateException( + "Cannot determine Elasticsearch DNS name. " + + "When using a custom network, set a network alias on the Elasticsearch container." + ); + } + + private ContainerNetwork findNetworkEndpoint(Map networks, Network network) { + // Try network ID first + ContainerNetwork endpoint = networks.get(network.getId()); + if (endpoint != null) { + return endpoint; + } + + // Try network name as fallback (Docker may key by name instead of ID) + try { + String networkName = DockerClientFactory + .instance() + .client() + .inspectNetworkCmd() + .withNetworkId(network.getId()) + .exec() + .getName(); + if (networkName != null) { + return networks.get(networkName); + } + } catch (Exception e) { + // Ignore and return null + } + + return null; + } + + private String requireElasticsearchContainerId() { + String id = elasticsearch.getContainerId(); + if (StringUtils.isBlank(id)) { + throw new IllegalStateException( + "Elasticsearch containerId is not available. In managed mode, Elasticsearch must be started via dependsOn(elasticsearch) " + + "before KibanaContainer is started." + ); + } + return id.trim(); + } + + private void connectRunningContainerToNetwork(String containerId, Network network) { + String networkId = network.getId(); + + try { + DockerClientFactory + .instance() + .client() + .connectToNetworkCmd() + .withContainerId(containerId) + .withNetworkId(networkId) + .exec(); + } catch (Exception e) { + throw new IllegalStateException("Failed to connect Elasticsearch container to ad-hoc shared network", e); + } + } + + private static void ensureCompatibleVersion(String esVersion) { + ComparableVersion comparableVersion = new ComparableVersion(esVersion); + if (comparableVersion.isLessThan(MINIMUM_SUPPORTED_VERSION)) { + throw new IllegalArgumentException( + String.format( + "Kibana version %s is not supported. Minimum version is %s", + comparableVersion, + MINIMUM_SUPPORTED_VERSION + ) + ); + } + } + + private String createKibanaServiceAccountToken(String protocol) { + if (elasticsearch == null) { + throw new IllegalStateException("Cannot create service account token in external mode"); + } + + String elasticPassword = elasticsearch + .getEnvMap() + .getOrDefault("ELASTIC_PASSWORD", ElasticsearchContainer.ELASTICSEARCH_DEFAULT_PASSWORD); + + // Create a unique token name to avoid collisions if the same ES container is reused. + String tokenName = "tc-kibana-" + Base58.randomString(12); + + String endpoint = protocol + "://localhost:9200/_security/service/elastic/kibana/credential/token/" + tokenName; + + return Unreliables.retryUntilSuccess( + 45, + TimeUnit.SECONDS, + () -> { + String curlTlsArgs = ""; + if ("https".equals(protocol)) { + // In managed HTTPS mode, use the Elasticsearch HTTP CA for curl. + curlTlsArgs = " --cacert '" + elasticsearch.getCertPath() + "'"; + } + + String curlCommand = String.format( + "curl -sS%s -u \"elastic:$1\" -H 'Content-Type: application/json' -X POST '%s'", + curlTlsArgs, + endpoint + ); + + Container.ExecResult result = elasticsearch.execInContainer( + "/bin/sh", + "-c", + curlCommand, + "sh", + elasticPassword + ); + + String stdout = (result.getStdout() == null) ? "" : result.getStdout(); + String stderr = (result.getStderr() == null) ? "" : result.getStderr(); + + if (result.getExitCode() != 0) { + throw new RuntimeException( + "Failed to create Kibana service account token. Exit code: " + + result.getExitCode() + + ", stdout: " + + stdout + + ", stderr: " + + stderr + ); + } + + JsonNode json = OBJECT_MAPPER.readTree(stdout); + JsonNode value = json.path("token").path("value"); + if (value.isTextual() && !value.asText().trim().isEmpty()) { + return value.asText().trim(); + } + + throw new RuntimeException("Service account token response did not contain token.value: " + stdout); + } + ); + } + + private static String stableConfigKey(DockerImageName imageName) { + // UUID v3 (name-based) gives a deterministic 32-character string from the image name, + // keeping xpack.encryptedSavedObjects.encryptionKey identical across JVM runs — + // a prerequisite for container reuse. Call withEncryptionKey() for real secret management. + return UUID + .nameUUIDFromBytes(imageName.asCanonicalNameString().getBytes(StandardCharsets.UTF_8)) + .toString() + .replace("-", ""); + } + + /** + * Returns the HTTP host address for accessing Kibana. + * + * @return the host address in the format "host:port" + */ + public String getHttpHostAddress() { + return getHost() + ":" + getMappedPort(KIBANA_DEFAULT_PORT); + } + + private void configureWaitStrategy() { + if (this.getWaitStrategy() != null) { + // the user might have set a custom wait strategy + return; + } + HttpWaitStrategy strategy = Wait + .forHttp("/api/status") + .forPort(KIBANA_DEFAULT_PORT) + .forStatusCode(200) + .forResponsePredicate(this::isKibanaReady); + + // Add authentication if we have Elasticsearch credentials available + String serviceToken = getEnvMap().get("ELASTICSEARCH_SERVICEACCOUNTTOKEN"); + String username = getEnvMap().get("ELASTICSEARCH_USERNAME"); + String password = getEnvMap().get("ELASTICSEARCH_PASSWORD"); + + if (serviceToken != null) { + strategy = strategy.withHeader("Authorization", "Bearer " + serviceToken); + } else if (username != null && password != null) { + strategy = strategy.withBasicCredentials(username, password); + } + + setWaitStrategy(strategy.withStartupTimeout(this.startupTimeout)); + } + + private boolean isKibanaReady(String body) { + try { + JsonNode json = OBJECT_MAPPER.readTree(body); + JsonNode status = json.path("status"); + + String overallLevel = status.path("overall").path("level").asText(null); + String elasticsearchLevel = status.path("core").path("elasticsearch").path("level").asText(null); + String savedObjectsLevel = status.path("core").path("savedObjects").path("level").asText(null); + + boolean overallAvailable = "available".equalsIgnoreCase(overallLevel); + boolean elasticsearchAvailable = "available".equalsIgnoreCase(elasticsearchLevel); + boolean savedObjectsAvailable = "available".equalsIgnoreCase(savedObjectsLevel); + + boolean isReady = overallAvailable && elasticsearchAvailable && savedObjectsAvailable; + + if (log.isDebugEnabled()) { + log.debug( + "Kibana status check: READY={} (overall={}, elasticsearch={}, savedObjects={})", + isReady, + overallLevel, + elasticsearchLevel, + savedObjectsLevel + ); + } + + return isReady; + } catch (Exception e) { + log.debug("Kibana status check: FAILED to parse response - {}", e.getMessage()); + return false; + } + } +} diff --git a/modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/ElasticsearchContainerTest.java b/modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/ElasticsearchContainerTest.java index 2b832f28a16..52cd78b4a34 100644 --- a/modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/ElasticsearchContainerTest.java +++ b/modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/ElasticsearchContainerTest.java @@ -16,13 +16,14 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.transport.client.PreBuiltTransportClient; -import org.junit.After; -import org.junit.Test; +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; @@ -33,7 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; -public class ElasticsearchContainerTest { +class ElasticsearchContainerTest { /** * Elasticsearch version which should be used for the Tests @@ -58,7 +59,7 @@ public class ElasticsearchContainerTest { private RestClient anonymousClient = null; - @After + @AfterEach public void stopRestClient() throws IOException { if (client != null) { client.close(); @@ -73,10 +74,10 @@ public void stopRestClient() throws IOException { @SuppressWarnings("deprecation") // Using deprecated constructor for verification of backwards compatibility @Test @Deprecated // We will remove this test in the future - public void elasticsearchDeprecatedCtorTest() throws IOException { + void elasticsearchDeprecatedCtorTest() throws IOException { // Create the elasticsearch container. try ( - ElasticsearchContainer container = new ElasticsearchContainer().withEnv("foo", "bar") // dummy env for compiler checking correct generics usage + 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(); @@ -95,7 +96,7 @@ public void elasticsearchDeprecatedCtorTest() throws IOException { } @Test - public void elasticsearchDefaultTest() throws IOException { + 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 @@ -117,7 +118,7 @@ public void elasticsearchDefaultTest() throws IOException { } @Test - public void elasticsearchSecuredTest() throws IOException { + void elasticsearchSecuredTest() throws IOException { try ( ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE) .withPassword(ELASTICSEARCH_PASSWORD) @@ -137,7 +138,7 @@ public void elasticsearchSecuredTest() throws IOException { } @Test - public void elasticsearchVersion() throws IOException { + void elasticsearchVersion() throws IOException { try (ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE)) { container.start(); Response response = getClient(container).performRequest(new Request("GET", "/")); @@ -148,7 +149,7 @@ public void elasticsearchVersion() throws IOException { } @Test - public void elasticsearchVersion83() throws IOException { + void elasticsearchVersion83() throws IOException { try ( ElasticsearchContainer container = new ElasticsearchContainer( "docker.elastic.co/elasticsearch/elasticsearch:8.3.0" @@ -162,7 +163,7 @@ public void elasticsearchVersion83() throws IOException { } @Test - public void elasticsearchOssImage() throws IOException { + void elasticsearchOssImage() throws IOException { try ( // ossContainer { ElasticsearchContainer container = new ElasticsearchContainer( @@ -181,8 +182,8 @@ public void elasticsearchOssImage() throws IOException { } @Test - public void restClientClusterHealth() throws IOException { - // httpClientContainer { + 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... @@ -207,13 +208,92 @@ public void restClientClusterHealth() throws IOException { // }} assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200); assertThat(EntityUtils.toString(response.getEntity())).contains("cluster_name"); - // httpClientContainer {{ + // httpClientContainer7 {{ } // } } @Test - public void restClientSecuredClusterHealth() throws IOException { + 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 ( @@ -250,7 +330,7 @@ public void restClientSecuredClusterHealth() throws IOException { @SuppressWarnings("deprecation") // The TransportClient will be removed in Elasticsearch 8. @Test - public void transportClientClusterHealth() { + void transportClientClusterHealth() { // transportClientContainer { // Create the elasticsearch container. try (ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE)) { @@ -276,7 +356,7 @@ public void transportClientClusterHealth() { } @Test - public void incompatibleSettingsTest() { + void incompatibleSettingsTest() { // The OSS image can not use security feature assertThat( catchThrowable(() -> { @@ -289,21 +369,7 @@ public void incompatibleSettingsTest() { } @Test - public void testElasticsearch8SecureByDefault() throws Exception { - try ( - ElasticsearchContainer container = new ElasticsearchContainer( - "docker.elastic.co/elasticsearch/elasticsearch:8.1.2" - ) - ) { - // Start the container. This step might take some time... - container.start(); - - assertClusterHealthResponse(container); - } - } - - @Test - public void testDockerHubElasticsearch8ImageSecureByDefault() throws Exception { + void testDockerHubElasticsearch8ImageSecureByDefault() throws Exception { try (ElasticsearchContainer container = new ElasticsearchContainer("elasticsearch:8.1.2")) { container.start(); @@ -312,7 +378,7 @@ public void testDockerHubElasticsearch8ImageSecureByDefault() throws Exception { } @Test - public void testElasticsearch8SecureByDefaultCustomCaCertFails() throws Exception { + void testElasticsearch8SecureByDefaultCustomCaCertFails() throws Exception { final MountableFile mountableFile = MountableFile.forClasspathResource("http_ca.crt"); String caPath = "/tmp/http_ca.crt"; try ( @@ -334,7 +400,7 @@ public void testElasticsearch8SecureByDefaultCustomCaCertFails() throws Exceptio } @Test - public void testElasticsearch8SecureByDefaultHttpWaitStrategy() throws Exception { + void testElasticsearch8SecureByDefaultHttpWaitStrategy() throws Exception { final HttpWaitStrategy httpsWaitStrategy = Wait .forHttps("/") .forPort(9200) @@ -357,7 +423,7 @@ public void testElasticsearch8SecureByDefaultHttpWaitStrategy() throws Exception } @Test - public void testElasticsearch8SecureByDefaultFailsSilentlyOnLatestImages() throws Exception { + 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 @@ -376,7 +442,50 @@ public void testElasticsearch8SecureByDefaultFailsSilentlyOnLatestImages() throw } @Test - public void testElasticsearchDefaultMaxHeapSize() throws Exception { + 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)) { @@ -386,7 +495,7 @@ public void testElasticsearchDefaultMaxHeapSize() throws Exception { } @Test - public void testElasticsearchCustomMaxHeapSizeInEnvironmentVariable() throws Exception { + void testElasticsearchCustomMaxHeapSizeInEnvironmentVariable() throws Exception { long customHeapSize = 1574961152; try ( @@ -399,7 +508,7 @@ public void testElasticsearchCustomMaxHeapSizeInEnvironmentVariable() throws Exc } @Test - public void testElasticsearchCustomMaxHeapSizeInJvmOptionsFile() throws Exception { + void testElasticsearchCustomMaxHeapSizeInJvmOptionsFile() throws Exception { long customHeapSize = 1574961152; try ( @@ -493,4 +602,37 @@ private void assertClusterHealthResponse(ElasticsearchContainer container) throw assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200); assertThat(EntityUtils.toString(response.getEntity())).contains("cluster_name"); } + + @Test + void testGetHttpSchemeForElasticsearch7ReturnsHttp() { + try (ElasticsearchContainer container = new ElasticsearchContainer(ELASTICSEARCH_IMAGE)) { + container.start(); + assertThat(container.getHttpScheme()).isEqualTo("http"); + } + } + + @Test + void testGetHttpSchemeForElasticsearch8ReturnsHttps() { + try ( + ElasticsearchContainer container = new ElasticsearchContainer( + "docker.elastic.co/elasticsearch/elasticsearch:8.1.2" + ) + ) { + container.start(); + assertThat(container.getHttpScheme()).isEqualTo("https"); + } + } + + @Test + void testGetHttpSchemeForElasticsearch8WithSslDisabledReturnsHttp() { + try ( + ElasticsearchContainer container = new ElasticsearchContainer( + "docker.elastic.co/elasticsearch/elasticsearch:8.1.2" + ) + .withEnv("xpack.security.http.ssl.enabled", "false") + ) { + container.start(); + assertThat(container.getHttpScheme()).isEqualTo("http"); + } + } } diff --git a/modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/KibanaContainerTest.java b/modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/KibanaContainerTest.java new file mode 100644 index 00000000000..0099b5613ca --- /dev/null +++ b/modules/elasticsearch/src/test/java/org/testcontainers/elasticsearch/KibanaContainerTest.java @@ -0,0 +1,515 @@ +package org.testcontainers.elasticsearch; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpResponse; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.util.EntityUtils; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.testcontainers.Testcontainers; +import org.testcontainers.containers.Container; +import org.testcontainers.containers.ContainerLaunchException; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.images.builder.Transferable; +import org.testcontainers.utility.TestcontainersConfiguration; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +class KibanaContainerTest { + + public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private static final String ES_IMAGE = "docker.elastic.co/elasticsearch/elasticsearch:9.2.4"; + + @Test + void cannotCreateKibanaContainerForVersionLessThan8() { + Assertions + .assertThatThrownBy(() -> new KibanaContainer("docker.elastic.co/kibana/kibana:7.17.29")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("is not supported"); + } + + @Test + void managedModeCanStartAndReachElasticsearchInSameExplicitNetwork() throws IOException { + // managedModeCanStartAndReachElasticsearchInSameExplicitNetwork { + try ( + Network network = Network.newNetwork(); + ElasticsearchContainer es = new ElasticsearchContainer(ES_IMAGE).withNetwork(network); + KibanaContainer kibana = new KibanaContainer(es).withNetwork(network) + ) { + es.start(); + kibana.start(); + + String status = getKibanaStatus(kibana); + Assertions.assertThat(status).isEqualTo("available"); + } + // } + } + + @Test + void managedModeCannotStartWithOnlyESNetworkExplicit() { + Network network = Network.newNetwork(); + try ( + ElasticsearchContainer es = new ElasticsearchContainer(ES_IMAGE).withNetwork(network); + KibanaContainer kibana = new KibanaContainer(es) + ) { + Assertions + .assertThatThrownBy(kibana::start) + .isInstanceOf(ContainerLaunchException.class) + .satisfies(ex -> { + Assertions + .assertThat(ex.getCause()) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("explicit network"); + }); + } + } + + @Test + void managedModeCannotStartWithDifferentExplicitNetworks() { + try ( + Network esNetwork = Network.newNetwork(); + Network kibanaNetwork = Network.newNetwork(); + ElasticsearchContainer es = new ElasticsearchContainer(ES_IMAGE).withNetwork(esNetwork); + KibanaContainer kibana = new KibanaContainer(es).withNetwork(kibanaNetwork) + ) { + Assertions + .assertThatThrownBy(kibana::start) + .isInstanceOf(ContainerLaunchException.class) + .satisfies(ex -> { + Assertions + .assertThat(ex.getCause()) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("different networks"); + }); + } + } + + @Test + void managedModeUsesCustomNetworkAliasInExplicitNetwork() throws Exception { + final String customEsAlias = "my-custom-es-alias"; + + try ( + Network network = Network.newNetwork(); + ElasticsearchContainer es = new ElasticsearchContainer(ES_IMAGE) + .withNetwork(network) + .withNetworkAliases(customEsAlias); + KibanaContainer kibana = new KibanaContainer(es).withNetwork(network) + ) { + kibana.start(); + + Assertions.assertThat(kibana.isRunning()).isTrue(); + + // Verify Kibana uses the custom alias (not auto-generated tc-* alias) + Container.ExecResult result = kibana.execInContainer("sh", "-c", "env | grep ELASTICSEARCH_HOSTS"); + + Assertions.assertThat(result.getStdout()).contains(customEsAlias).contains(":9200"); + } + } + + @Test + void managedModeCanStartAndReachElasticsearchWithoutExplicitNetwork() throws IOException { + try ( + ElasticsearchContainer es = new ElasticsearchContainer(ES_IMAGE); + KibanaContainer kibana = new KibanaContainer(es) + ) { + kibana.start(); + + String status = getKibanaStatus(kibana); + Assertions.assertThat(status).isEqualTo("available"); + } + } + + @Test + void managedModeCanStartWithoutElasticsearchSecurity() throws IOException { + try ( + ElasticsearchContainer es = new ElasticsearchContainer(ES_IMAGE).withEnv("xpack.security.enabled", "false"); + KibanaContainer kibana = new KibanaContainer(es) + ) { + kibana.start(); + + String status = getKibanaStatus(kibana); + Assertions.assertThat(status).isEqualTo("available"); + } + } + + @Test + void managedModeCanStartWithoutElasticsearchHttps() throws IOException { + try ( + ElasticsearchContainer es = new ElasticsearchContainer(ES_IMAGE) + .withEnv("xpack.security.enabled", "true") + .withEnv("xpack.security.http.ssl.enabled", "false"); + KibanaContainer kibana = new KibanaContainer(es) + ) { + es.start(); + kibana.start(); + + String status = getKibanaStatus(kibana); + Assertions.assertThat(status).isEqualTo("available"); + } + } + + @Test + void externalModeFailsWithConflictingCredentials() { + Assertions + .assertThatThrownBy(() -> { + new KibanaContainer("docker.elastic.co/kibana/kibana:8.0.0") + .withKibanaUsernameAndPassword("user", "pass") + .withElasticsearchServiceAccountToken("token"); + }) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Conflicting Elasticsearch credentials"); + } + + @Test + void managedModeFailsWhenSettingElasticsearchUrl() { + ElasticsearchContainer es = new ElasticsearchContainer(ES_IMAGE); + Assertions + .assertThatThrownBy(() -> { + new KibanaContainer(es).withElasticsearchUrl("http://somewhere.over.the.rainbow:9200"); + }) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Cannot set Elasticsearch URL when using Elasticsearch container"); + } + + @Test + void failsWhenNoElasticsearchConfigured() { + try (KibanaContainer kibana = new KibanaContainer("docker.elastic.co/kibana/kibana:8.0.0")) { + Assertions + .assertThatThrownBy(kibana::start) + .isInstanceOf(ContainerLaunchException.class) + .satisfies(ex -> { + Assertions + .assertThat(ex.getCause()) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Elasticsearch must be configured"); + }); + } + } + + @Test + void externalModeCanWorkWithUsernamePassword() throws IOException, InterruptedException { + final String esHostname = "elasticsearch"; + + // externalModeCanWorkWithUsernamePassword { + try ( + Network network = Network.newNetwork(); + ElasticsearchContainer es = new ElasticsearchContainer(ES_IMAGE) + .withNetwork(network) + .withNetworkAliases(esHostname) + .withEnv("xpack.security.http.ssl.enabled", "false") + ) { + es.start(); + String kibanaSystemPassword = setKibanaSystemPassword(es); + + try ( + KibanaContainer kibana = new KibanaContainer("docker.elastic.co/kibana/kibana:9.2.2") //this minor version is intentionally below ES version + .withNetwork(network) + .withElasticsearchUrl("http://" + esHostname + ":9200") + .withKibanaSystemPassword(kibanaSystemPassword) + ) { + kibana.start(); + String status = getKibanaStatus(kibana); + Assertions.assertThat(status).isEqualTo("available"); + } + } + // } + } + + @Test + void externalModeCanWorkWithoutCredentials() throws IOException { + final String esHostname = "elasticsearch"; + + try ( + Network network = Network.newNetwork(); + ElasticsearchContainer es = new ElasticsearchContainer(ES_IMAGE) + .withNetwork(network) + .withNetworkAliases(esHostname) + .withEnv("xpack.security.enabled", "false") + .withEnv("xpack.security.http.ssl.enabled", "false"); + KibanaContainer kibana = new KibanaContainer("docker.elastic.co/kibana/kibana:9.2.4") + .withNetwork(network) + .withElasticsearchUrl("http://" + esHostname + ":9200") + ) { + es.start(); + kibana.start(); + String status = getKibanaStatus(kibana); + Assertions.assertThat(status).isEqualTo("available"); + } + } + + @Test + void externalModeCanStartAndReachElasticsearchWithCertAndServiceToken() throws Exception { + byte[] caCrt; + byte[] nodeCrt; + byte[] nodeKey; + + String esHostname = "elasticsearch"; + + String instancesYml = + "instances:\n" + + " - name: es01\n" + + " dns: [ \"localhost\", \"" + + esHostname + + "\", \"es01\" ]\n" + + " ip: [ \"127.0.0.1\" ]\n"; + + try ( + ElasticsearchContainer setup = new ElasticsearchContainer(ES_IMAGE) + .withEnv("discovery.type", "single-node") + .withCopyToContainer( + Transferable.of(instancesYml.getBytes(StandardCharsets.UTF_8), 0644), + "/tmp/instances.yml" + ) + ) { + setup.start(); + + // Run certutil inside the running container and write outputs inside the container FS + Container.ExecResult execResult = setup.execInContainer( + "bash", + "-lc", + "set -euo pipefail && " + + "mkdir -p /tmp/out && " + + "cd /usr/share/elasticsearch && " + + "bin/elasticsearch-certutil ca --silent --pem --out /tmp/out/ca.zip && " + + "unzip -o /tmp/out/ca.zip -d /tmp/out && " + + "bin/elasticsearch-certutil cert --silent --pem --in /tmp/instances.yml --ca-cert /tmp/out/ca/ca.crt --ca-key /tmp/out/ca/ca.key --out /tmp/out/certs.zip && " + + "unzip -o /tmp/out/certs.zip -d /tmp/out" + ); + Assertions.assertThat(execResult.getExitCode()).isEqualTo(0); + + // copy the certificates and key from the container, so we can use them later + caCrt = setup.copyFileFromContainer("/tmp/out/ca/ca.crt", IOUtils::toByteArray); + nodeCrt = setup.copyFileFromContainer("/tmp/out/es01/es01.crt", IOUtils::toByteArray); + nodeKey = setup.copyFileFromContainer("/tmp/out/es01/es01.key", IOUtils::toByteArray); + } + + try ( + Network network = Network.newNetwork(); + ElasticsearchContainer es = new ElasticsearchContainer( + "docker.elastic.co/elasticsearch/elasticsearch:9.2.4" + ) + .withNetwork(network) + .withNetworkAliases(esHostname) + ) { + applyTls(es, caCrt, nodeCrt, nodeKey); + es.start(); + String kibanaServiceAccountToken = createKibanaServiceAccountToken(es); + + try ( + KibanaContainer kibana = new KibanaContainer("docker.elastic.co/kibana/kibana:9.2.4") + // network is needed only because the ES we try to access via explicit mode is operated by non-public Docker + .withNetwork(network) + .withElasticsearchUrl("https://" + esHostname + ":9200") + .withElasticsearchServiceAccountToken(kibanaServiceAccountToken) + .withElasticsearchCaCertificate(es.caCertAsBytes().get()) + ) { + kibana.start(); + String status = getKibanaStatus(kibana); + Assertions.assertThat(status).isEqualTo("available"); + } + } + } + + private static String setKibanaSystemPassword(ElasticsearchContainer elasticsearch) throws IOException { + String kibanaPassword = "kibana-system-" + System.currentTimeMillis(); + + try (CloseableHttpClient httpClient = createHttpClient(elasticsearch)) { + String url = String.format( + "%s://%s/_security/user/kibana_system/_password", + elasticsearch.getHttpScheme(), + elasticsearch.getHttpHostAddress() + ); + HttpPost request = new HttpPost(url); + request.setHeader("Content-Type", "application/json"); + request.setEntity(new StringEntity("{\"password\":\"" + kibanaPassword + "\"}")); + + HttpResponse response = httpClient.execute(request); + int statusCode = response.getStatusLine().getStatusCode(); + String body = EntityUtils.toString(response.getEntity()); + + if (statusCode != 200) { + throw new IllegalStateException( + "Failed to set kibana_system password. HTTP " + statusCode + ", body=" + body + ); + } + + // ES 9.x returns {} on success; older versions may return {"acknowledged":true} + // Just validate that the body is valid JSON. + try { + OBJECT_MAPPER.readTree(body.isEmpty() ? "{}" : body); + } catch (IOException e) { + throw new IllegalStateException("Non-JSON response body: " + body, e); + } + + return kibanaPassword; + } + } + + private static String createKibanaServiceAccountToken(ElasticsearchContainer elasticsearch) throws IOException { + String tokenName = "kibana-token-" + System.currentTimeMillis(); + + try (CloseableHttpClient httpClient = createHttpClient(elasticsearch)) { + String url = String.format( + "%s://%s/_security/service/elastic/kibana/credential/token/%s", + elasticsearch.getHttpScheme(), + elasticsearch.getHttpHostAddress(), + tokenName + ); + HttpPost request = new HttpPost(url); + request.setHeader("Content-Type", "application/json"); + + HttpResponse response = httpClient.execute(request); + int statusCode = response.getStatusLine().getStatusCode(); + String body = EntityUtils.toString(response.getEntity()); + + if (statusCode != 200) { + throw new IllegalStateException( + "Failed to create Kibana service account token. HTTP " + statusCode + ", body=" + body + ); + } + + // Expected JSON: + // {"created":true,"token":{"name":"...","value":"AAEAA..."}} + try { + JsonNode root = OBJECT_MAPPER.readTree(body); + JsonNode tokenValue = root.path("token").path("value"); + + if (tokenValue.isMissingNode() || tokenValue.isNull()) { + throw new IllegalStateException("Token value not found in response: " + body); + } + + return tokenValue.asText(); + } catch (IOException e) { + throw new IllegalStateException("Failed to parse token response: " + body, e); + } + } + } + + private static CloseableHttpClient createHttpClient(ElasticsearchContainer elasticsearch) { + String elasticPassword = elasticsearch.getEnvMap().get("ELASTIC_PASSWORD"); + HttpClientBuilder clientBuilder = HttpClientBuilder.create(); + + if (StringUtils.isNotBlank(elasticPassword)) { + CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials( + AuthScope.ANY, + new UsernamePasswordCredentials("elastic", elasticPassword) + ); + clientBuilder.setDefaultCredentialsProvider(credentialsProvider); + } + + String scheme = elasticsearch.getHttpScheme(); + if ("https".equals(scheme)) { + SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory( + elasticsearch.createSslContextFromCa() + ); + clientBuilder.setSSLSocketFactory(sslSocketFactory); + } + + return clientBuilder.build(); + } + + private static String getKibanaStatus(KibanaContainer kibana) throws IOException { + try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) { + String url = "http://" + kibana.getHttpHostAddress() + "/api/status"; + HttpResponse response = httpClient.execute(new org.apache.http.client.methods.HttpGet(url)); + int statusCode = response.getStatusLine().getStatusCode(); + String body = EntityUtils.toString(response.getEntity()); + + if (statusCode != 200) { + throw new IllegalStateException("Failed to get Kibana status. HTTP " + statusCode + ", body=" + body); + } + + JsonNode json = OBJECT_MAPPER.readTree(body); + String status = json.path("status").path("overall").path("level").asText(null); + if (status == null) { + throw new IllegalStateException("Kibana status response missing 'status.overall.level' field: " + body); + } + return status; + } + } + + @Test + void withReuseShouldReuseTheSameContainer() { + Assumptions + .assumeThat(TestcontainersConfiguration.getInstance().environmentSupportsReuse()) + .as("testcontainers.reuse.enable must be true") + .isTrue(); + + final String kibanaImage = "docker.elastic.co/kibana/kibana:9.2.4"; + + // Testcontainers.exposeHostPorts + host.testcontainers.internal lets Kibana reach ES + // from inside the container on any platform (Linux Docker Engine included). + // No withNetwork() on Kibana keeps the hash fully deterministic: + // - no dynamic network ID + // - the random tc-* alias added by GenericContainer's constructor is only serialised + // into the CreateContainerCmd when withNetwork() has been called, so it is absent here + // The host.testcontainers.internal extra-host IP is the same for kibana1 and kibana2 + // because they start in the same JVM (same PortForwardingContainer instance). + // The first Kibana container must stay running while the second one starts, because + // withReuse(true) only skips JVM-shutdown cleanup — an explicit stop() still removes the + // container, so there would be nothing to find. + try ( + ElasticsearchContainer es = new ElasticsearchContainer(ES_IMAGE) + .withEnv("xpack.security.enabled", "false") + .withEnv("xpack.security.http.ssl.enabled", "false") + ) { + es.start(); + int esMappedPort = es.getMappedPort(9200); + Testcontainers.exposeHostPorts(esMappedPort); + String esUrl = "http://" + GenericContainer.INTERNAL_HOST_HOSTNAME + ":" + esMappedPort; + + KibanaContainer kibana1 = new KibanaContainer(kibanaImage).withElasticsearchUrl(esUrl).withReuse(true); + KibanaContainer kibana2 = new KibanaContainer(kibanaImage).withElasticsearchUrl(esUrl).withReuse(true); + + try { + kibana1.start(); + // kibana2 is started while kibana1 is still running; the reuse mechanism should + // find kibana1's container by hash and return the same container ID. + kibana2.start(); + + Assertions + .assertThat(kibana2.getContainerId()) + .as("KibanaContainer with withReuse(true) should reuse the same container on subsequent starts") + .isEqualTo(kibana1.getContainerId()); + } finally { + kibana1.stop(); + kibana2.stop(); + } + } + } + + private static void applyTls(ElasticsearchContainer c, byte[] caCrt, byte[] nodeCrt, byte[] nodeKey) { + final String certDir = "/usr/share/elasticsearch/config/certs"; + + // Copy provided materials + c.withCopyToContainer(Transferable.of(caCrt, 0644), certDir + "/http_ca.crt"); + c.withCopyToContainer(Transferable.of(nodeCrt, 0644), certDir + "/http.crt"); + c.withCopyToContainer(Transferable.of(nodeKey, 0644), certDir + "/http.key"); + + // Disable ES bootstrap TLS autoconfiguration + c.withEnv("xpack.security.autoconfiguration.enabled", "false"); + c.withEnv("xpack.security.enabled", "true"); + + // Configure ONLY HTTP TLS using exactly the provided files + c.withEnv("xpack.security.http.ssl.enabled", "true"); + c.withEnv("xpack.security.http.ssl.certificate_authorities", "certs/http_ca.crt"); + c.withEnv("xpack.security.http.ssl.certificate", "certs/http.crt"); + c.withEnv("xpack.security.http.ssl.key", "certs/http.key"); + } +} diff --git a/modules/gcloud/build.gradle b/modules/gcloud/build.gradle index 30f74c94b70..3a020bdb759 100644 --- a/modules/gcloud/build.gradle +++ b/modules/gcloud/build.gradle @@ -3,12 +3,11 @@ description = "Testcontainers :: GCloud" dependencies { api project(':testcontainers') - testImplementation platform("com.google.cloud:libraries-bom:26.30.0") + testImplementation platform("com.google.cloud:libraries-bom:26.84.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' - testImplementation 'org.assertj:assertj-core:3.25.1' } diff --git a/modules/gcloud/src/main/java/org/testcontainers/containers/BigQueryEmulatorContainer.java b/modules/gcloud/src/main/java/org/testcontainers/containers/BigQueryEmulatorContainer.java index 6c305a50537..6590c6cab5b 100644 --- a/modules/gcloud/src/main/java/org/testcontainers/containers/BigQueryEmulatorContainer.java +++ b/modules/gcloud/src/main/java/org/testcontainers/containers/BigQueryEmulatorContainer.java @@ -7,7 +7,10 @@ *

* 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"); @@ -33,6 +36,10 @@ 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; } diff --git a/modules/gcloud/src/main/java/org/testcontainers/containers/BigtableEmulatorContainer.java b/modules/gcloud/src/main/java/org/testcontainers/containers/BigtableEmulatorContainer.java index 0325b58e745..288cd499e37 100644 --- a/modules/gcloud/src/main/java/org/testcontainers/containers/BigtableEmulatorContainer.java +++ b/modules/gcloud/src/main/java/org/testcontainers/containers/BigtableEmulatorContainer.java @@ -1,6 +1,6 @@ package org.testcontainers.containers; -import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; +import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; /** @@ -9,7 +9,10 @@ * 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( @@ -33,7 +36,7 @@ public BigtableEmulatorContainer(final DockerImageName dockerImageName) { dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, CLOUD_SDK_IMAGE_NAME); withExposedPorts(PORT); - setWaitStrategy(new LogMessageWaitStrategy().withRegEx("(?s).*running.*$")); + setWaitStrategy(Wait.forLogMessage(".*running.*$", 1)); withCommand("/bin/sh", "-c", CMD); } diff --git a/modules/gcloud/src/main/java/org/testcontainers/containers/DatastoreEmulatorContainer.java b/modules/gcloud/src/main/java/org/testcontainers/containers/DatastoreEmulatorContainer.java index eb86fa3932f..cbe27e303f4 100644 --- a/modules/gcloud/src/main/java/org/testcontainers/containers/DatastoreEmulatorContainer.java +++ b/modules/gcloud/src/main/java/org/testcontainers/containers/DatastoreEmulatorContainer.java @@ -9,7 +9,10 @@ * 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( diff --git a/modules/gcloud/src/main/java/org/testcontainers/containers/FirestoreEmulatorContainer.java b/modules/gcloud/src/main/java/org/testcontainers/containers/FirestoreEmulatorContainer.java index 1d3ec4a01d1..3c8d12f82ad 100644 --- a/modules/gcloud/src/main/java/org/testcontainers/containers/FirestoreEmulatorContainer.java +++ b/modules/gcloud/src/main/java/org/testcontainers/containers/FirestoreEmulatorContainer.java @@ -1,6 +1,6 @@ package org.testcontainers.containers; -import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; +import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; /** @@ -9,7 +9,10 @@ * 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( @@ -24,6 +27,8 @@ public class FirestoreEmulatorContainer extends GenericContainer * 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( @@ -33,7 +36,7 @@ public PubSubEmulatorContainer(final DockerImageName dockerImageName) { dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, CLOUD_SDK_IMAGE_NAME); withExposedPorts(8085); - setWaitStrategy(new LogMessageWaitStrategy().withRegEx("(?s).*started.*$")); + setWaitStrategy(Wait.forLogMessage(".*started.*$", 1)); withCommand("/bin/sh", "-c", CMD); } diff --git a/modules/gcloud/src/main/java/org/testcontainers/containers/SpannerEmulatorContainer.java b/modules/gcloud/src/main/java/org/testcontainers/containers/SpannerEmulatorContainer.java index ff36bddd1b2..76a95d4c4ac 100644 --- a/modules/gcloud/src/main/java/org/testcontainers/containers/SpannerEmulatorContainer.java +++ b/modules/gcloud/src/main/java/org/testcontainers/containers/SpannerEmulatorContainer.java @@ -1,13 +1,16 @@ package org.testcontainers.containers; -import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; +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( @@ -27,7 +30,7 @@ public SpannerEmulatorContainer(final DockerImageName dockerImageName) { dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); withExposedPorts(GRPC_PORT, HTTP_PORT); - setWaitStrategy(new LogMessageWaitStrategy().withRegEx(".*Cloud Spanner emulator running\\..*")); + setWaitStrategy(Wait.forLogMessage(".*Cloud Spanner emulator running\\..*", 1)); } /** diff --git a/modules/gcloud/src/main/java/org/testcontainers/gcloud/BigQueryEmulatorContainer.java b/modules/gcloud/src/main/java/org/testcontainers/gcloud/BigQueryEmulatorContainer.java new file mode 100644 index 00000000000..b5d00bf374a --- /dev/null +++ b/modules/gcloud/src/main/java/org/testcontainers/gcloud/BigQueryEmulatorContainer.java @@ -0,0 +1,44 @@ +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; + } +} diff --git a/modules/gcloud/src/main/java/org/testcontainers/gcloud/BigtableEmulatorContainer.java b/modules/gcloud/src/main/java/org/testcontainers/gcloud/BigtableEmulatorContainer.java new file mode 100644 index 00000000000..b1e62ae0aa1 --- /dev/null +++ b/modules/gcloud/src/main/java/org/testcontainers/gcloud/BigtableEmulatorContainer.java @@ -0,0 +1,53 @@ +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); + } +} diff --git a/modules/gcloud/src/main/java/org/testcontainers/gcloud/DatastoreEmulatorContainer.java b/modules/gcloud/src/main/java/org/testcontainers/gcloud/DatastoreEmulatorContainer.java new file mode 100644 index 00000000000..360aedce686 --- /dev/null +++ b/modules/gcloud/src/main/java/org/testcontainers/gcloud/DatastoreEmulatorContainer.java @@ -0,0 +1,73 @@ +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; + } +} diff --git a/modules/gcloud/src/main/java/org/testcontainers/gcloud/FirestoreEmulatorContainer.java b/modules/gcloud/src/main/java/org/testcontainers/gcloud/FirestoreEmulatorContainer.java new file mode 100644 index 00000000000..140f7166307 --- /dev/null +++ b/modules/gcloud/src/main/java/org/testcontainers/gcloud/FirestoreEmulatorContainer.java @@ -0,0 +1,64 @@ +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); + } +} diff --git a/modules/gcloud/src/main/java/org/testcontainers/gcloud/PubSubEmulatorContainer.java b/modules/gcloud/src/main/java/org/testcontainers/gcloud/PubSubEmulatorContainer.java new file mode 100644 index 00000000000..417ee8dc524 --- /dev/null +++ b/modules/gcloud/src/main/java/org/testcontainers/gcloud/PubSubEmulatorContainer.java @@ -0,0 +1,49 @@ +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); + } +} diff --git a/modules/gcloud/src/main/java/org/testcontainers/gcloud/SpannerEmulatorContainer.java b/modules/gcloud/src/main/java/org/testcontainers/gcloud/SpannerEmulatorContainer.java new file mode 100644 index 00000000000..aa99f6be5f4 --- /dev/null +++ b/modules/gcloud/src/main/java/org/testcontainers/gcloud/SpannerEmulatorContainer.java @@ -0,0 +1,50 @@ +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); + } +} diff --git a/modules/gcloud/src/test/java/org/testcontainers/containers/BigQueryEmulatorContainerTest.java b/modules/gcloud/src/test/java/org/testcontainers/containers/BigQueryEmulatorContainerTest.java deleted file mode 100644 index 01fcbe137a6..00000000000 --- a/modules/gcloud/src/test/java/org/testcontainers/containers/BigQueryEmulatorContainerTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.testcontainers.containers; - -import com.google.cloud.NoCredentials; -import com.google.cloud.bigquery.BigQuery; -import com.google.cloud.bigquery.BigQueryOptions; -import com.google.cloud.bigquery.QueryJobConfiguration; -import com.google.cloud.bigquery.TableResult; -import org.junit.Test; - -import java.math.BigDecimal; -import java.util.List; -import java.util.stream.Collectors; - -import static org.assertj.core.api.Assertions.assertThat; - -public class BigQueryEmulatorContainerTest { - - @Test - public void test() 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)); - } - } -} diff --git a/modules/gcloud/src/test/java/org/testcontainers/containers/BigtableEmulatorContainerTest.java b/modules/gcloud/src/test/java/org/testcontainers/containers/BigtableEmulatorContainerTest.java deleted file mode 100644 index e68ecce405b..00000000000 --- a/modules/gcloud/src/test/java/org/testcontainers/containers/BigtableEmulatorContainerTest.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.testcontainers.containers; - -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.models.Row; -import com.google.cloud.bigtable.data.v2.models.RowCell; -import com.google.cloud.bigtable.data.v2.models.RowMutation; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; -import org.junit.Rule; -import org.junit.Test; -import org.testcontainers.utility.DockerImageName; - -import java.io.IOException; -import java.util.List; -import java.util.concurrent.ExecutionException; - -import static org.assertj.core.api.Assertions.assertThat; - -public class BigtableEmulatorContainerTest { - - public static final String PROJECT_ID = "test-project"; - - public static final String INSTANCE_ID = "test-instance"; - - @Rule - // emulatorContainer { - public BigtableEmulatorContainer emulator = new BigtableEmulatorContainer( - DockerImageName.parse("gcr.io/google.com/cloudsdktool/google-cloud-cli:441.0.0-emulators") - ); - - // } - - @Test - // testWithEmulatorContainer { - public void testSimple() throws IOException, InterruptedException, ExecutionException { - ManagedChannel channel = ManagedChannelBuilder.forTarget(emulator.getEmulatorEndpoint()).usePlaintext().build(); - - TransportChannelProvider channelProvider = FixedTransportChannelProvider.create( - GrpcTransportChannel.create(channel) - ); - NoCredentialsProvider credentialsProvider = NoCredentialsProvider.create(); - - try { - createTable(channelProvider, credentialsProvider, "test-table"); - - BigtableDataClient client = BigtableDataClient.create( - BigtableDataSettings - .newBuilderForEmulator(emulator.getHost(), emulator.getEmulatorPort()) - .setProjectId(PROJECT_ID) - .setInstanceId(INSTANCE_ID) - .build() - ); - - client.mutateRow(RowMutation.create("test-table", "1").setCell("name", "firstName", "Ray")); - - Row row = client.readRow("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 { - EnhancedBigtableTableAdminStub stub = EnhancedBigtableTableAdminStub.createEnhanced( - BigtableTableAdminStubSettings - .newBuilder() - .setTransportChannelProvider(channelProvider) - .setCredentialsProvider(credentialsProvider) - .build() - ); - - try (BigtableTableAdminClient client = BigtableTableAdminClient.create(PROJECT_ID, INSTANCE_ID, stub)) { - Table table = client.createTable(CreateTableRequest.of(tableName).addFamily("name")); - } - } - // } -} diff --git a/modules/gcloud/src/test/java/org/testcontainers/containers/FirestoreEmulatorContainerTest.java b/modules/gcloud/src/test/java/org/testcontainers/containers/FirestoreEmulatorContainerTest.java deleted file mode 100644 index 0162085ea35..00000000000 --- a/modules/gcloud/src/test/java/org/testcontainers/containers/FirestoreEmulatorContainerTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.testcontainers.containers; - -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.Rule; -import org.junit.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; - -public class FirestoreEmulatorContainerTest { - - @Rule - // emulatorContainer { - public FirestoreEmulatorContainer emulator = new FirestoreEmulatorContainer( - DockerImageName.parse("gcr.io/google.com/cloudsdktool/google-cloud-cli:441.0.0-emulators") - ); - - // } - - // testWithEmulatorContainer { - @Test - public void testSimple() throws ExecutionException, InterruptedException { - 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"); - } - // } - -} diff --git a/modules/gcloud/src/test/java/org/testcontainers/gcloud/BigQueryEmulatorContainerTest.java b/modules/gcloud/src/test/java/org/testcontainers/gcloud/BigQueryEmulatorContainerTest.java new file mode 100644 index 00000000000..7d657830fa4 --- /dev/null +++ b/modules/gcloud/src/test/java/org/testcontainers/gcloud/BigQueryEmulatorContainerTest.java @@ -0,0 +1,204 @@ +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(); + } +} diff --git a/modules/gcloud/src/test/java/org/testcontainers/gcloud/BigtableEmulatorContainerTest.java b/modules/gcloud/src/test/java/org/testcontainers/gcloud/BigtableEmulatorContainerTest.java new file mode 100644 index 00000000000..64f19a42df5 --- /dev/null +++ b/modules/gcloud/src/test/java/org/testcontainers/gcloud/BigtableEmulatorContainerTest.java @@ -0,0 +1,102 @@ +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")); + } + } + // } +} diff --git a/modules/gcloud/src/test/java/org/testcontainers/containers/DatastoreEmulatorContainerTest.java b/modules/gcloud/src/test/java/org/testcontainers/gcloud/DatastoreEmulatorContainerTest.java similarity index 56% rename from modules/gcloud/src/test/java/org/testcontainers/containers/DatastoreEmulatorContainerTest.java rename to modules/gcloud/src/test/java/org/testcontainers/gcloud/DatastoreEmulatorContainerTest.java index 7ea0ceb9a49..dceec1af215 100644 --- a/modules/gcloud/src/test/java/org/testcontainers/containers/DatastoreEmulatorContainerTest.java +++ b/modules/gcloud/src/test/java/org/testcontainers/gcloud/DatastoreEmulatorContainerTest.java @@ -1,4 +1,4 @@ -package org.testcontainers.containers; +package org.testcontainers.gcloud; import com.google.cloud.NoCredentials; import com.google.cloud.ServiceOptions; @@ -6,8 +6,7 @@ import com.google.cloud.datastore.DatastoreOptions; import com.google.cloud.datastore.Entity; import com.google.cloud.datastore.Key; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.utility.DockerImageName; import java.io.IOException; @@ -16,37 +15,38 @@ public class DatastoreEmulatorContainerTest { - @Rule - // creatingDatastoreEmulatorContainer { - public DatastoreEmulatorContainer emulator = new DatastoreEmulatorContainer( - DockerImageName.parse("gcr.io/google.com/cloudsdktool/google-cloud-cli:441.0.0-emulators") - ); - - // } - // startingDatastoreEmulatorContainer { @Test public void testSimple() { - DatastoreOptions options = DatastoreOptions - .newBuilder() - .setHost(emulator.getEmulatorEndpoint()) - .setCredentials(NoCredentials.getInstance()) - .setRetrySettings(ServiceOptions.getNoRetrySettings()) - .setProjectId(emulator.getProjectId()) - .build(); - Datastore datastore = options.getService(); + 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); + 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"); + assertThat(datastore.get(key).getString("description")).isEqualTo("my description"); + } } // } @Test - public void testWithFlags() throws IOException, InterruptedException { + void testWithFlags() throws IOException, InterruptedException { try ( DatastoreEmulatorContainer emulator = new DatastoreEmulatorContainer( "gcr.io/google.com/cloudsdktool/google-cloud-cli:441.0.0-emulators" @@ -61,7 +61,7 @@ public void testWithFlags() throws IOException, InterruptedException { } @Test - public void testWithMultipleFlags() throws IOException, InterruptedException { + void testWithMultipleFlags() throws IOException, InterruptedException { try ( DatastoreEmulatorContainer emulator = new DatastoreEmulatorContainer( "gcr.io/google.com/cloudsdktool/google-cloud-cli:441.0.0-emulators" diff --git a/modules/gcloud/src/test/java/org/testcontainers/gcloud/FirestoreEmulatorContainerTest.java b/modules/gcloud/src/test/java/org/testcontainers/gcloud/FirestoreEmulatorContainerTest.java new file mode 100644 index 00000000000..2c3233388aa --- /dev/null +++ b/modules/gcloud/src/test/java/org/testcontainers/gcloud/FirestoreEmulatorContainerTest.java @@ -0,0 +1,73 @@ +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")); + } + } +} diff --git a/modules/gcloud/src/test/java/org/testcontainers/containers/PubSubEmulatorContainerTest.java b/modules/gcloud/src/test/java/org/testcontainers/gcloud/PubSubEmulatorContainerTest.java similarity index 52% rename from modules/gcloud/src/test/java/org/testcontainers/containers/PubSubEmulatorContainerTest.java rename to modules/gcloud/src/test/java/org/testcontainers/gcloud/PubSubEmulatorContainerTest.java index b2f75f0f36a..eb3ed0260ee 100644 --- a/modules/gcloud/src/test/java/org/testcontainers/containers/PubSubEmulatorContainerTest.java +++ b/modules/gcloud/src/test/java/org/testcontainers/gcloud/PubSubEmulatorContainerTest.java @@ -1,4 +1,4 @@ -package org.testcontainers.containers; +package org.testcontainers.gcloud; import com.google.api.gax.core.NoCredentialsProvider; import com.google.api.gax.grpc.GrpcTransportChannel; @@ -22,70 +22,73 @@ import com.google.pubsub.v1.TopicName; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; -import org.junit.Rule; -import org.junit.Test; +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 PubSubEmulatorContainerTest { +class PubSubEmulatorContainerTest { - public static final String PROJECT_ID = "my-project-id"; - - @Rule - // emulatorContainer { - public PubSubEmulatorContainer emulator = new PubSubEmulatorContainer( - DockerImageName.parse("gcr.io/google.com/cloudsdktool/google-cloud-cli:441.0.0-emulators") - ); - - // } + private static final String PROJECT_ID = "my-project-id"; @Test // testWithEmulatorContainer { - public void testSimple() throws IOException { - String hostport = emulator.getEmulatorEndpoint(); - ManagedChannel channel = ManagedChannelBuilder.forTarget(hostport).usePlaintext().build(); - try { - TransportChannelProvider channelProvider = FixedTransportChannelProvider.create( - GrpcTransportChannel.create(channel) + void testSimple() throws IOException { + try ( + // emulatorContainer { + PubSubEmulatorContainer emulator = new PubSubEmulatorContainer( + DockerImageName.parse("gcr.io/google.com/cloudsdktool/google-cloud-cli:441.0.0-emulators") ); - 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 + // } + ) { + 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() - .setMaxMessages(1) - .setSubscription(ProjectSubscriptionName.format(PROJECT_ID, subscriptionId)) + .setData(ByteString.copyFromUtf8("test message")) .build(); - PullResponse pullResponse = subscriber.pullCallable().call(pullRequest); + publisher.publish(message); - assertThat(pullResponse.getReceivedMessagesList()).hasSize(1); - assertThat(pullResponse.getReceivedMessages(0).getMessage().getData().toStringUtf8()) - .isEqualTo("test 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(); } - } finally { - channel.shutdown(); } } diff --git a/modules/gcloud/src/test/java/org/testcontainers/containers/SpannerEmulatorContainerTest.java b/modules/gcloud/src/test/java/org/testcontainers/gcloud/SpannerEmulatorContainerTest.java similarity index 56% rename from modules/gcloud/src/test/java/org/testcontainers/containers/SpannerEmulatorContainerTest.java rename to modules/gcloud/src/test/java/org/testcontainers/gcloud/SpannerEmulatorContainerTest.java index b246e7e1f0d..2d6ac963882 100644 --- a/modules/gcloud/src/test/java/org/testcontainers/containers/SpannerEmulatorContainerTest.java +++ b/modules/gcloud/src/test/java/org/testcontainers/gcloud/SpannerEmulatorContainerTest.java @@ -1,4 +1,4 @@ -package org.testcontainers.containers; +package org.testcontainers.gcloud; import com.google.cloud.NoCredentials; import com.google.cloud.spanner.Database; @@ -14,8 +14,7 @@ import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerOptions; import com.google.cloud.spanner.Statement; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.utility.DockerImageName; import java.util.Arrays; @@ -23,15 +22,7 @@ import static org.assertj.core.api.Assertions.assertThat; -public class SpannerEmulatorContainerTest { - - @Rule - // emulatorContainer { - public SpannerEmulatorContainer emulator = new SpannerEmulatorContainer( - DockerImageName.parse("gcr.io/cloud-spanner-emulator/emulator:1.4.0") - ); - - // } +class SpannerEmulatorContainerTest { private static final String PROJECT_NAME = "test-project"; @@ -41,38 +32,47 @@ public class SpannerEmulatorContainerTest { // testWithEmulatorContainer { @Test - public void testSimple() throws ExecutionException, InterruptedException { - 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"); + 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"); + } } // } diff --git a/modules/grafana/build.gradle b/modules/grafana/build.gradle new file mode 100644 index 00000000000..93bae9750b4 --- /dev/null +++ b/modules/grafana/build.gradle @@ -0,0 +1,14 @@ +description = "Testcontainers :: Grafana" + +dependencies { + api project(':testcontainers') + + testImplementation 'io.rest-assured:rest-assured:5.5.7' + testImplementation 'io.micrometer:micrometer-registry-otlp:1.17.0' + testImplementation 'uk.org.webcompere:system-stubs-jupiter:2.1.8' + + testImplementation platform('io.opentelemetry:opentelemetry-bom:1.65.0') + testImplementation 'io.opentelemetry:opentelemetry-api' + testImplementation 'io.opentelemetry:opentelemetry-sdk' + testImplementation 'io.opentelemetry:opentelemetry-exporter-otlp' +} diff --git a/modules/grafana/src/main/java/org/testcontainers/grafana/LgtmStackContainer.java b/modules/grafana/src/main/java/org/testcontainers/grafana/LgtmStackContainer.java new file mode 100644 index 00000000000..52443d7ba8a --- /dev/null +++ b/modules/grafana/src/main/java/org/testcontainers/grafana/LgtmStackContainer.java @@ -0,0 +1,81 @@ +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); + } +} diff --git a/modules/grafana/src/test/java/org/testcontainers/grafana/LgtmStackContainerTest.java b/modules/grafana/src/test/java/org/testcontainers/grafana/LgtmStackContainerTest.java new file mode 100644 index 00000000000..ff8d5cb6609 --- /dev/null +++ b/modules/grafana/src/test/java/org/testcontainers/grafana/LgtmStackContainerTest.java @@ -0,0 +1,156 @@ +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; + } + }; + } +} diff --git a/modules/grafana/src/test/resources/logback-test.xml b/modules/grafana/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..83ef7a1a3ef --- /dev/null +++ b/modules/grafana/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + diff --git a/modules/hivemq/build.gradle b/modules/hivemq/build.gradle index 7aca932667b..13ec90ff167 100644 --- a/modules/hivemq/build.gradle +++ b/modules/hivemq/build.gradle @@ -2,30 +2,23 @@ description = "Testcontainers :: HiveMQ" dependencies { api(project(":testcontainers")) - api("org.jetbrains:annotations:24.1.0") + api("org.jetbrains:annotations:26.1.0") - shaded("org.apache.commons:commons-lang3:3.14.0") - shaded("commons-io:commons-io:2.15.1") - shaded("org.javassist:javassist:3.30.2-GA") + shaded("org.apache.commons:commons-lang3:3.20.0") + shaded("commons-io:commons-io:2.21.0") + shaded("org.javassist:javassist:3.32.0-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") + shaded("net.lingala.zip4j:zip4j:2.11.6") - testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.1") - testImplementation(project(":junit-jupiter")) - testImplementation("com.hivemq:hivemq-extension-sdk:4.24.0") - testImplementation("com.hivemq:hivemq-mqtt-client:1.3.3") + testImplementation(project(":testcontainers-junit-jupiter")) + testImplementation("com.hivemq:hivemq-extension-sdk:4.50.0") + testImplementation("com.hivemq:hivemq-mqtt-client:1.3.15") testImplementation("org.apache.httpcomponents:httpclient:4.5.14") - testImplementation("ch.qos.logback:logback-classic:1.4.14") - testImplementation 'org.assertj:assertj-core:3.25.1' - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.1") + testImplementation("ch.qos.logback:logback-classic:1.5.37") } test { - useJUnitPlatform() - testLogging { - events "passed", "skipped", "failed" - } javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(11) } diff --git a/modules/hivemq/src/main/java/org/testcontainers/hivemq/HiveMQContainer.java b/modules/hivemq/src/main/java/org/testcontainers/hivemq/HiveMQContainer.java index 4638f758745..241d5b18c9e 100644 --- a/modules/hivemq/src/main/java/org/testcontainers/hivemq/HiveMQContainer.java +++ b/modules/hivemq/src/main/java/org/testcontainers/hivemq/HiveMQContainer.java @@ -8,7 +8,7 @@ import org.slf4j.event.Level; import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; +import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.containers.wait.strategy.WaitAllStrategy; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; @@ -82,7 +82,7 @@ public HiveMQContainer(final @NotNull DockerImageName dockerImageName) { addExposedPort(MQTT_PORT); - waitStrategy.withStrategy(new LogMessageWaitStrategy().withRegEx("(.*)Started HiveMQ in(.*)")); + waitStrategy.withStrategy(Wait.forLogMessage("(.*)Started HiveMQ in(.*)", 1)); waitingFor(waitStrategy); withLogConsumer(outputFrame -> { @@ -134,9 +134,9 @@ protected void configure() { 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" + "cp -r '/opt/hivemq/temp-extensions/'* /opt/hivemq/extensions/ ; " + + "chmod -R 777 /opt/hivemq/extensions ; " + + "/opt/docker-entrypoint.sh /opt/hivemq/bin/run.sh" ); } @@ -160,7 +160,7 @@ protected void containerIsStarted(final @NotNull InspectContainerResponse contai */ public @NotNull HiveMQContainer waitForExtension(final @NotNull String extensionName) { final String regEX = "(.*)Extension \"" + extensionName + "\" version (.*) started successfully(.*)"; - waitStrategy.withStrategy(new LogMessageWaitStrategy().withRegEx(regEX)); + waitStrategy.withStrategy(Wait.forLogMessage(regEX, 1)); return self(); } diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionFromDirectoryIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionFromDirectoryIT.java index 6fef56c0a2d..a5b6170a228 100755 --- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionFromDirectoryIT.java +++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionFromDirectoryIT.java @@ -1,7 +1,10 @@ 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; @@ -11,12 +14,18 @@ class ContainerWithExtensionFromDirectoryIT { - @Test + @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() throws Exception { + void test(final @NotNull String hivemqCeTag) throws Exception { try ( final HiveMQContainer hivemq = new HiveMQContainer( - DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3") + DockerImageName.parse("hivemq/hivemq-ce").withTag(hivemqCeTag) ) .withExtension(MountableFile.forClasspathResource("/modifier-extension")) .waitForExtension("Modifier Extension") @@ -33,7 +42,7 @@ void test() throws Exception { void test_wrongDirectoryName() throws Exception { try ( final HiveMQContainer hivemq = new HiveMQContainer( - DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3") + DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3") ) .withExtension(MountableFile.forClasspathResource("/modifier-extension-wrong-name")) .waitForExtension("Modifier Extension") diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionIT.java index eda3f236e2c..2b255912da9 100755 --- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionIT.java +++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionIT.java @@ -1,7 +1,9 @@ package org.testcontainers.hivemq; -import org.junit.jupiter.api.Test; +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; @@ -11,9 +13,15 @@ class ContainerWithExtensionIT { - @Test + @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() throws Exception { + void test(final @NotNull String hivemqCeTag) throws Exception { final HiveMQExtension hiveMQExtension = HiveMQExtension .builder() .id("extension-1") @@ -24,7 +32,7 @@ void test() throws Exception { try ( final HiveMQContainer hivemq = new HiveMQContainer( - DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3") + DockerImageName.parse("hivemq/hivemq-ce").withTag(hivemqCeTag) ) .withHiveMQConfig(MountableFile.forClasspathResource("/inMemoryConfig.xml")) .waitForExtension(hiveMQExtension) diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionSubclassIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionSubclassIT.java index cd1df9ce8e7..a907b8f2d2e 100755 --- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionSubclassIT.java +++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithExtensionSubclassIT.java @@ -25,7 +25,7 @@ void test() throws Exception { try ( final HiveMQContainer hivemq = new HiveMQContainer( - DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3") + DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3") ) .waitForExtension(hiveMQExtension) .withExtension(hiveMQExtension) diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithFileInExtensionHomeIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithFileInExtensionHomeIT.java index dde5e677a15..c3c74b0dce7 100755 --- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithFileInExtensionHomeIT.java +++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithFileInExtensionHomeIT.java @@ -9,8 +9,9 @@ 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.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; @@ -22,9 +23,15 @@ class ContainerWithFileInExtensionHomeIT { - @Test + @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() throws Exception { + void test(final @NotNull String hivemqCeTag) throws Exception { final HiveMQExtension hiveMQExtension = HiveMQExtension .builder() .id("extension-1") @@ -35,7 +42,7 @@ void test() throws Exception { try ( final HiveMQContainer hivemq = new HiveMQContainer( - DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3") + DockerImageName.parse("hivemq/hivemq-ce").withTag(hivemqCeTag) ) .withHiveMQConfig(MountableFile.forClasspathResource("/inMemoryConfig.xml")) .withExtension(hiveMQExtension) diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithFileInHomeIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithFileInHomeIT.java index 78a51398bf3..c8e19316788 100755 --- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithFileInHomeIT.java +++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithFileInHomeIT.java @@ -35,7 +35,7 @@ void test() throws Exception { try ( final HiveMQContainer hivemq = new HiveMQContainer( - DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3") + DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3") ) .withHiveMQConfig(MountableFile.forClasspathResource("/inMemoryConfig.xml")) .withExtension(hiveMQExtension) diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithLicenseIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithLicenseIT.java index f6aa6ed08f0..3973abddb0b 100755 --- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithLicenseIT.java +++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/ContainerWithLicenseIT.java @@ -35,7 +35,7 @@ void test() throws Exception { try ( final HiveMQContainer hivemq = new HiveMQContainer( - DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3") + DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3") ) .withHiveMQConfig(MountableFile.forClasspathResource("/inMemoryConfig.xml")) .withExtension(hiveMQExtension) diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/CreateFileInCopiedDirectoryIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/CreateFileInCopiedDirectoryIT.java index c0e95f61cf6..df0f3ef3b9c 100755 --- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/CreateFileInCopiedDirectoryIT.java +++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/CreateFileInCopiedDirectoryIT.java @@ -47,7 +47,7 @@ void test() throws Exception { try ( final HiveMQContainer hivemq = new HiveMQContainer( - DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3") + DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3") ) .withHiveMQConfig(MountableFile.forClasspathResource("/inMemoryConfig.xml")) .withExtension(extension) diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/CreateFileInExtensionDirectoryIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/CreateFileInExtensionDirectoryIT.java index 3e7bec94e6e..6f65ebb1e9f 100755 --- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/CreateFileInExtensionDirectoryIT.java +++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/CreateFileInExtensionDirectoryIT.java @@ -9,8 +9,9 @@ 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.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; @@ -23,9 +24,15 @@ class CreateFileInExtensionDirectoryIT { - @Test + @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() throws Exception { + void test(final @NotNull String hivemqCeTag) throws Exception { final HiveMQExtension hiveMQExtension = HiveMQExtension .builder() .id("extension-1") @@ -36,7 +43,7 @@ void test() throws Exception { try ( final HiveMQContainer hivemq = new HiveMQContainer( - DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3") + DockerImageName.parse("hivemq/hivemq-ce").withTag(hivemqCeTag) ) .withHiveMQConfig(MountableFile.forClasspathResource("/inMemoryConfig.xml")) .waitForExtension(hiveMQExtension) diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/HiveMQTestContainerCore.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/HiveMQTestContainerCore.java index 32097e52641..ede972e957d 100644 --- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/HiveMQTestContainerCore.java +++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/HiveMQTestContainerCore.java @@ -16,7 +16,7 @@ class HiveMQTestContainerCore { @NotNull - final HiveMQContainer container = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3")); + final HiveMQContainer container = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3")); @TempDir File tempDir; diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoExtensionTestsIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoExtensionTestsIT.java index f3289413a36..a1ff664649d 100644 --- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoExtensionTestsIT.java +++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoExtensionTestsIT.java @@ -38,7 +38,7 @@ class DemoExtensionTestsIT { @Container final HiveMQContainer hivemqWithClasspathExtension = new HiveMQContainer( - DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3") + DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3") ) .waitForExtension(hiveMQEClasspathxtension) .withExtension(hiveMQEClasspathxtension) diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoFilesIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoFilesIT.java index a76fa60576f..8fe08b24750 100644 --- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoFilesIT.java +++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoFilesIT.java @@ -19,7 +19,7 @@ class DemoFilesIT { // hivemqHome { final HiveMQContainer hivemqFileInHome = new HiveMQContainer( - DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3") + DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3") ) .withFileInHomeFolder( MountableFile.forHostPath("src/test/resources/additionalFile.txt"), @@ -31,7 +31,7 @@ class DemoFilesIT { // extensionHome { @Container final HiveMQContainer hivemqFileInExtensionHome = new HiveMQContainer( - DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3") + DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3") ) .withExtension( HiveMQExtension @@ -52,7 +52,7 @@ class DemoFilesIT { // withLicenses { @Container - final HiveMQContainer hivemq = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3")) + 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")); diff --git a/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoHiveMQContainerIT.java b/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoHiveMQContainerIT.java index db6e3e58251..ed063617537 100644 --- a/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoHiveMQContainerIT.java +++ b/modules/hivemq/src/test/java/org/testcontainers/hivemq/docs/DemoHiveMQContainerIT.java @@ -18,7 +18,7 @@ class DemoHiveMQContainerIT { // ceVersion { @Container - final HiveMQContainer hivemqCe = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq-ce").withTag("2021.3")) + final HiveMQContainer hivemqCe = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq-ce").withTag("2024.3")) .withLogLevel(Level.DEBUG); // } @@ -43,7 +43,7 @@ class DemoHiveMQContainerIT { // specificVersion { @Container - final HiveMQContainer hivemqSpecificVersion = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq-ce:2021.3")); + final HiveMQContainer hivemqSpecificVersion = new HiveMQContainer(DockerImageName.parse("hivemq/hivemq-ce:2024.3")); // } diff --git a/modules/influxdb/build.gradle b/modules/influxdb/build.gradle index 01f902d92e7..145aa4aac52 100644 --- a/modules/influxdb/build.gradle +++ b/modules/influxdb/build.gradle @@ -3,9 +3,8 @@ description = "Testcontainers :: InfluxDB" dependencies { api project(':testcontainers') - compileOnly 'org.influxdb:influxdb-java:2.24' + compileOnly 'org.influxdb:influxdb-java:2.25' - testImplementation 'org.assertj:assertj-core:3.25.1' - testImplementation 'org.influxdb:influxdb-java:2.24' - testImplementation "com.influxdb:influxdb-client-java:6.12.0" + testImplementation 'org.influxdb:influxdb-java:2.25' + testImplementation "com.influxdb:influxdb-client-java:7.5.0" } diff --git a/modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerTest.java b/modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerTest.java index a3862fb7242..53678fa6dce 100644 --- a/modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerTest.java +++ b/modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerTest.java @@ -11,7 +11,7 @@ import com.influxdb.client.write.Point; import com.influxdb.query.FluxRecord; import com.influxdb.query.FluxTable; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.utility.DockerImageName; import java.time.Instant; @@ -20,7 +20,7 @@ import static org.assertj.core.api.Assertions.assertThat; -public class InfluxDBContainerTest { +class InfluxDBContainerTest { private static final String USERNAME = "new-test-user"; @@ -37,7 +37,7 @@ public class InfluxDBContainerTest { private static final int SECONDS_IN_WEEK = 604800; @Test - public void getInfluxDBClient() { + void getInfluxDBClient() { try ( // constructorWithDefaultVariables { final InfluxDBContainer influxDBContainer = new InfluxDBContainer<>( @@ -55,7 +55,7 @@ public void getInfluxDBClient() { } @Test - public void getInfluxDBClientWithAdminToken() { + void getInfluxDBClientWithAdminToken() { try ( // constructorWithAdminToken { final InfluxDBContainer influxDBContainer = new InfluxDBContainer<>( @@ -81,7 +81,7 @@ public void getInfluxDBClientWithAdminToken() { } @Test - public void getBucket() { + void getBucket() { try ( // constructorWithCustomVariables { final InfluxDBContainer influxDBContainer = new InfluxDBContainer<>( @@ -111,7 +111,7 @@ public void getBucket() { } @Test - public void queryForWriteAndRead() { + void queryForWriteAndRead() { try ( final InfluxDBContainer influxDBContainer = new InfluxDBContainer<>( InfluxDBTestUtils.INFLUXDB_V2_TEST_IMAGE diff --git a/modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerV1Test.java b/modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerV1Test.java index c9e292c5675..ea074cee68e 100644 --- a/modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerV1Test.java +++ b/modules/influxdb/src/test/java/org/testcontainers/containers/InfluxDBContainerV1Test.java @@ -5,14 +5,14 @@ import org.influxdb.dto.Point; import org.influxdb.dto.Query; import org.influxdb.dto.QueryResult; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.utility.DockerImageName; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; -public class InfluxDBContainerV1Test { +class InfluxDBContainerV1Test { private static final String TEST_VERSION = InfluxDBTestUtils.INFLUXDB_V1_TEST_IMAGE.getVersionPart(); @@ -23,7 +23,7 @@ public class InfluxDBContainerV1Test { private static final String PASSWORD = "new-test-password"; @Test - public void createInfluxDBOnlyWithUrlAndCorrectVersion() { + void createInfluxDBOnlyWithUrlAndCorrectVersion() { try ( // constructorWithDefaultVariables { final InfluxDBContainer influxDBContainer = new InfluxDBContainer<>( @@ -43,7 +43,7 @@ public void createInfluxDBOnlyWithUrlAndCorrectVersion() { } @Test - public void getNewInfluxDBWithCorrectVersion() { + void getNewInfluxDBWithCorrectVersion() { try ( final InfluxDBContainer influxDBContainer = new InfluxDBContainer<>( InfluxDBTestUtils.INFLUXDB_V1_TEST_IMAGE @@ -61,7 +61,7 @@ public void getNewInfluxDBWithCorrectVersion() { } @Test - public void describeDatabases() { + void describeDatabases() { try ( // constructorWithUserPassword { final InfluxDBContainer influxDBContainer = new InfluxDBContainer<>( @@ -82,7 +82,7 @@ public void describeDatabases() { } @Test - public void queryForWriteAndRead() { + void queryForWriteAndRead() { try ( final InfluxDBContainer influxDBContainer = new InfluxDBContainer<>( InfluxDBTestUtils.INFLUXDB_V1_TEST_IMAGE diff --git a/modules/jdbc-test/build.gradle b/modules/jdbc-test/build.gradle index 01f1b35632a..44fe705a255 100644 --- a/modules/jdbc-test/build.gradle +++ b/modules/jdbc-test/build.gradle @@ -1,16 +1,15 @@ dependencies { - api project(':jdbc') + api project(':testcontainers-jdbc') - api 'com.google.guava:guava:33.0.0-jre' - api 'org.apache.commons:commons-lang3:3.14.0' + 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 'com.googlecode.junit-toolbox:junit-toolbox:2.4' + api 'org.assertj:assertj-core:3.27.7' - api 'org.assertj:assertj-core:3.25.1' - - api 'org.apache.tomcat:tomcat-jdbc:10.0.27' - api 'org.vibur:vibur-dbcp:25.0' - api 'mysql:mysql-connector-java:8.0.33' + api 'org.apache.tomcat:tomcat-jdbc:11.0.23' + api 'org.vibur:vibur-dbcp:26.0' + api 'com.mysql:mysql-connector-j:9.6.0' + api 'org.junit.jupiter:junit-jupiter:5.14.3' } diff --git a/modules/jdbc-test/src/main/java/org/testcontainers/jdbc/AbstractJDBCDriverTest.java b/modules/jdbc-test/src/main/java/org/testcontainers/jdbc/AbstractJDBCDriverTest.java index a22b4e24ed0..95f9d5d1ad4 100644 --- a/modules/jdbc-test/src/main/java/org/testcontainers/jdbc/AbstractJDBCDriverTest.java +++ b/modules/jdbc-test/src/main/java/org/testcontainers/jdbc/AbstractJDBCDriverTest.java @@ -4,9 +4,11 @@ import com.zaxxer.hikari.HikariDataSource; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.lang3.SystemUtils; -import org.junit.AfterClass; -import org.junit.Test; -import org.junit.runners.Parameterized.Parameter; +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; @@ -15,8 +17,10 @@ import java.util.EnumSet; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assume.assumeFalse; +import static org.assertj.core.api.Assumptions.assumeThat; +@ParameterizedClass +@MethodSource("data") public class AbstractJDBCDriverTest { protected enum Options { @@ -27,7 +31,7 @@ protected enum Options { PmdKnownBroken, } - @Parameter + @Parameter(0) public String jdbcUrl; @Parameter(1) @@ -39,13 +43,13 @@ public static void sampleInitFunction(Connection connection) throws SQLException connection.createStatement().execute("CREATE TABLE my_counter (\n" + " n INT\n" + ");"); } - @AfterClass + @AfterAll public static void testCleanup() { ContainerDatabaseDriver.killContainers(); } @Test - public void test() throws SQLException { + void test() throws SQLException { try (HikariDataSource dataSource = getDataSource(jdbcUrl, 1)) { performSimpleTest(dataSource); @@ -130,7 +134,8 @@ private void performTestForJDBCParamUsage(HikariDataSource dataSource) throws SQ if ( databaseType.equalsIgnoreCase("postgresql") || databaseType.equalsIgnoreCase("postgis") || - databaseType.equalsIgnoreCase("timescaledb") + databaseType.equalsIgnoreCase("timescaledb") || + databaseType.equalsIgnoreCase("pgvector") ) { databaseQuery = "SELECT CURRENT_DATABASE()"; } @@ -202,7 +207,7 @@ private HikariDataSource verifyCharacterSet(String jdbcUrl) throws SQLException } private void performTestForCustomIniFile(HikariDataSource dataSource) throws SQLException { - assumeFalse(SystemUtils.IS_OS_WINDOWS); + assumeThat(SystemUtils.IS_OS_WINDOWS).isFalse(); Statement statement = dataSource.getConnection().createStatement(); statement.execute("SELECT @@GLOBAL.innodb_max_undo_log_size"); ResultSet resultSet = statement.getResultSet(); diff --git a/modules/jdbc/build.gradle b/modules/jdbc/build.gradle index 927d275c312..4d538fe2720 100644 --- a/modules/jdbc/build.gradle +++ b/modules/jdbc/build.gradle @@ -1,14 +1,13 @@ description = "Testcontainers :: JDBC" dependencies { - api project(':database-commons') + api project(':testcontainers-database-commons') - compileOnly 'org.jetbrains:annotations:24.1.0' + compileOnly 'org.jetbrains:annotations:26.1.0' testImplementation 'commons-dbutils:commons-dbutils:1.8.1' - testImplementation 'org.vibur:vibur-dbcp:25.0' - testImplementation 'org.apache.tomcat:tomcat-jdbc:10.1.18' + testImplementation 'org.vibur:vibur-dbcp:26.0' + testImplementation 'org.apache.tomcat:tomcat-jdbc:11.0.21' testImplementation 'com.zaxxer:HikariCP-java6:2.3.13' - testImplementation 'org.assertj:assertj-core:3.25.1' testImplementation ('org.mockito:mockito-core:4.11.0') { exclude(module: 'hamcrest-core') } diff --git a/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java b/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java index 9533a4c7d10..cf6c995528f 100644 --- a/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java +++ b/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java @@ -17,10 +17,15 @@ 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; /** @@ -34,7 +39,7 @@ public abstract class JdbcDatabaseContainer initScriptPaths = new ArrayList<>(); protected Map parameters = new HashMap<>(); @@ -132,8 +137,37 @@ public SELF withConnectTimeoutSeconds(int connectTimeoutSeconds) { return self(); } + /** + * Sets a script for initialization. + * + * @param initScriptPath path to the script file + * @return self + */ public SELF withInitScript(String initScriptPath) { - this.initScriptPath = 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(); } @@ -148,10 +182,10 @@ protected void waitUntilContainerStarted() { ); // Repeatedly try and open a connection to the DB and execute a test query - long start = System.currentTimeMillis(); + long start = System.nanoTime(); Exception lastConnectionException = null; - while (System.currentTimeMillis() < start + (1000 * startupTimeoutSeconds)) { + while ((System.nanoTime() - start) < TimeUnit.SECONDS.toNanos(startupTimeoutSeconds)) { if (!isRunning()) { Thread.sleep(100L); } else { @@ -238,9 +272,9 @@ public Connection createConnection(String queryString, Properties info) SQLException lastException = null; try { - long start = System.currentTimeMillis(); + long start = System.nanoTime(); // give up if we hit the time limit or the container stops running for some reason - while (System.currentTimeMillis() < start + (1000 * connectTimeoutSeconds) && isRunning()) { + while ((System.nanoTime() - start < TimeUnit.SECONDS.toNanos(connectTimeoutSeconds)) && isRunning()) { try { logger() .debug( @@ -328,9 +362,10 @@ protected void optionallyMapResourceParameterAsVolume( * Load init script content and apply it to the database if initScriptPath is set */ protected void runInitScriptIfRequired() { - if (initScriptPath != null) { - ScriptUtils.runInitScript(getDatabaseDelegate(), initScriptPath); - } + initScriptPaths + .stream() + .filter(Objects::nonNull) + .forEach(path -> ScriptUtils.runInitScript(getDatabaseDelegate(), path)); } public void setParameters(Map parameters) { diff --git a/modules/jdbc/src/main/java/org/testcontainers/jdbc/ConnectionUrl.java b/modules/jdbc/src/main/java/org/testcontainers/jdbc/ConnectionUrl.java index e1309f66197..ec606f3ed32 100644 --- a/modules/jdbc/src/main/java/org/testcontainers/jdbc/ConnectionUrl.java +++ b/modules/jdbc/src/main/java/org/testcontainers/jdbc/ConnectionUrl.java @@ -107,7 +107,7 @@ private void parseUrl() { //In case it matches to the default pattern Matcher dbInstanceMatcher = Patterns.DB_INSTANCE_MATCHING_PATTERN.matcher(dbHostString); if (dbInstanceMatcher.matches()) { - databaseHost = Optional.of(dbInstanceMatcher.group("databaseHost")); + databaseHost = Optional.ofNullable(dbInstanceMatcher.group("databaseHost")); databasePort = Optional.ofNullable(dbInstanceMatcher.group("databasePort")).map(Integer::valueOf); databaseName = Optional.of(dbInstanceMatcher.group("databaseName")); } @@ -227,7 +227,7 @@ public interface Patterns { //Matches to part of string - hostname:port/databasename Pattern DB_INSTANCE_MATCHING_PATTERN = Pattern.compile( - "(?[^:]+)" + + "(?[^:]+)?" + "(:(?[0-9]+))?" + "(" + "(?[:/])" + diff --git a/modules/jdbc/src/main/java/org/testcontainers/jdbc/JdbcDatabaseDelegate.java b/modules/jdbc/src/main/java/org/testcontainers/jdbc/JdbcDatabaseDelegate.java index 3c33eba8d5b..24c18944292 100644 --- a/modules/jdbc/src/main/java/org/testcontainers/jdbc/JdbcDatabaseDelegate.java +++ b/modules/jdbc/src/main/java/org/testcontainers/jdbc/JdbcDatabaseDelegate.java @@ -6,6 +6,7 @@ import org.testcontainers.exception.ConnectionCreationException; import org.testcontainers.ext.ScriptUtils; +import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; @@ -17,6 +18,8 @@ public class JdbcDatabaseDelegate extends AbstractDatabaseDelegate { private JdbcDatabaseContainer container; + private Connection connection; + private String queryString; public JdbcDatabaseDelegate(JdbcDatabaseContainer container, String queryString) { @@ -27,7 +30,8 @@ public JdbcDatabaseDelegate(JdbcDatabaseContainer container, String queryString) @Override protected Statement createNewConnection() { try { - return container.createConnection(queryString).createStatement(); + 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); @@ -65,6 +69,7 @@ public void execute( protected void closeConnectionQuietly(Statement statement) { try { statement.close(); + connection.close(); } catch (Exception e) { log.error("Could not close JDBC connection", e); } diff --git a/modules/jdbc/src/test/java/org/testcontainers/containers/JdbcDatabaseContainerTest.java b/modules/jdbc/src/test/java/org/testcontainers/containers/JdbcDatabaseContainerTest.java index 1dc6646bd17..ca41c3f5d1c 100644 --- a/modules/jdbc/src/test/java/org/testcontainers/containers/JdbcDatabaseContainerTest.java +++ b/modules/jdbc/src/test/java/org/testcontainers/containers/JdbcDatabaseContainerTest.java @@ -1,7 +1,7 @@ package org.testcontainers.containers; import lombok.NonNull; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.slf4j.Logger; import java.sql.Connection; @@ -10,10 +10,10 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.Mockito.mock; -public class JdbcDatabaseContainerTest { +class JdbcDatabaseContainerTest { @Test - public void anExceptionIsThrownIfJdbcIsNotAvailable() { + void anExceptionIsThrownIfJdbcIsNotAvailable() { JdbcDatabaseContainer jdbcContainer = new JdbcDatabaseContainerStub("mysql:latest") .withStartupTimeoutSeconds(1); diff --git a/modules/jdbc/src/test/java/org/testcontainers/jdbc/ConnectionUrlDriversTests.java b/modules/jdbc/src/test/java/org/testcontainers/jdbc/ConnectionUrlDriversTests.java index 394451bc6a9..5797ee0083c 100644 --- a/modules/jdbc/src/test/java/org/testcontainers/jdbc/ConnectionUrlDriversTests.java +++ b/modules/jdbc/src/test/java/org/testcontainers/jdbc/ConnectionUrlDriversTests.java @@ -1,127 +1,126 @@ package org.testcontainers.jdbc; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameter; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; -import java.util.Arrays; 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. */ -@RunWith(Parameterized.class) -public class ConnectionUrlDriversTests { +class ConnectionUrlDriversTests { - @Parameter - public String jdbcUrl; - - @Parameter(1) - public String databaseType; - - @Parameter(2) - public Optional tag; - - @Parameter(3) - public String dbHostString; - - @Parameter(4) - public String databaseName; - - @Parameterized.Parameters(name = "{index} - {0}") - public static Iterable data() { - return Arrays.asList( - new Object[][] { - { "jdbc:tc:mysql:8.0.36://hostname/test", "mysql", Optional.of("8.0.36"), "hostname/test", "test" }, - { "jdbc:tc:mysql://hostname/test", "mysql", Optional.empty(), "hostname/test", "test" }, - { - "jdbc:tc:postgresql:1.2.3://hostname/test", - "postgresql", - Optional.of("1.2.3"), - "hostname/test", - "test", - }, - { "jdbc:tc:postgresql://hostname/test", "postgresql", Optional.empty(), "hostname/test", "test" }, - { - "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", - }, - { - "jdbc:tc:sqlserver://localhost;instance=SQLEXPRESS:1433;databaseName=test", - "sqlserver", - Optional.empty(), - "localhost;instance=SQLEXPRESS:1433;databaseName=test", - "test", - }, - { - "jdbc:tc:mariadb:1.2.3://localhost:3306/test", - "mariadb", - Optional.of("1.2.3"), - "localhost:3306/test", - "test", - }, - { "jdbc:tc:mariadb://localhost:3306/test", "mariadb", Optional.empty(), "localhost:3306/test", "test" }, - { - "jdbc:tc:oracle:1.2.3:thin://@localhost:1521/test", - "oracle", - Optional.of("1.2.3"), - "localhost:1521/test", - "test", - }, - { - "jdbc:tc:oracle:1.2.3:thin:@localhost:1521/test", - "oracle", - Optional.of("1.2.3"), - "localhost:1521/test", - "test", - }, - { - "jdbc:tc:oracle:thin:@localhost:1521/test", - "oracle", - Optional.empty(), - "localhost:1521/test", - "test", - }, - { - "jdbc:tc:oracle:1.2.3:thin:@localhost:1521:test", - "oracle", - Optional.of("1.2.3"), - "localhost:1521:test", - "test", - }, - { - "jdbc:tc:oracle:1.2.3:thin://@localhost:1521:test", - "oracle", - Optional.of("1.2.3"), - "localhost:1521:test", - "test", - }, - { - "jdbc:tc:oracle:1.2.3-anything:thin://@localhost:1521:test", - "oracle", - Optional.of("1.2.3-anything"), - "localhost:1521:test", - "test", - }, - { - "jdbc:tc:oracle:thin:@localhost:1521:test", - "oracle", - Optional.empty(), - "localhost:1521:test", - "test", - }, - } + 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" + ) ); } - @Test - public void 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); diff --git a/modules/jdbc/src/test/java/org/testcontainers/jdbc/ConnectionUrlTest.java b/modules/jdbc/src/test/java/org/testcontainers/jdbc/ConnectionUrlTest.java index 123690d95f6..16abff74ec9 100644 --- a/modules/jdbc/src/test/java/org/testcontainers/jdbc/ConnectionUrlTest.java +++ b/modules/jdbc/src/test/java/org/testcontainers/jdbc/ConnectionUrlTest.java @@ -1,18 +1,14 @@ package org.testcontainers.jdbc; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; -public class ConnectionUrlTest { - - @Rule - public ExpectedException thrown = ExpectedException.none(); +class ConnectionUrlTest { @Test - public void testConnectionUrl1() { + void testConnectionUrl1() { String urlString = "jdbc:tc:mysql:8.0.36://somehostname:3306/databasename?a=b&c=d"; ConnectionUrl url = ConnectionUrl.newInstance(urlString); @@ -31,7 +27,7 @@ public void testConnectionUrl1() { } @Test - public void testConnectionUrl2() { + void testConnectionUrl2() { String urlString = "jdbc:tc:mysql://somehostname/databasename"; ConnectionUrl url = ConnectionUrl.newInstance(urlString); @@ -49,13 +45,13 @@ public void testConnectionUrl2() { } @Test - public void testEmptyQueryParameter() { + void testEmptyQueryParameter() { ConnectionUrl url = ConnectionUrl.newInstance("jdbc:tc:mysql://somehostname/databasename?key="); assertThat(url.getQueryParameters().get("key")).as("'key' property value").isEqualTo(""); } @Test - public void testTmpfsOption() { + void testTmpfsOption() { String urlString = "jdbc:tc:mysql://somehostname/databasename?TC_TMPFS=key:value,key1:value1"; ConnectionUrl url = ConnectionUrl.newInstance(urlString); @@ -69,7 +65,7 @@ public void testTmpfsOption() { } @Test - public void testInitScriptPathCapture() { + 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); @@ -83,13 +79,14 @@ public void testInitScriptPathCapture() { .containsEntry("TC_INITSCRIPT", "somepath/init_mysql.sql"); //Parameter sets are unmodifiable - thrown.expect(UnsupportedOperationException.class); - url.getContainerParameters().remove("TC_INITSCRIPT"); - url.getQueryParameters().remove("a"); + assertThatThrownBy(() -> url.getContainerParameters().remove("TC_INITSCRIPT")) + .isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(() -> url.getQueryParameters().remove("a")) + .isInstanceOf(UnsupportedOperationException.class); } @Test - public void testInitFunctionCapture() { + 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); @@ -105,10 +102,26 @@ public void testInitFunctionCapture() { } @Test - public void testDaemonCapture() { + 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"); + } } diff --git a/modules/jdbc/src/test/java/org/testcontainers/jdbc/ContainerDatabaseDriverTest.java b/modules/jdbc/src/test/java/org/testcontainers/jdbc/ContainerDatabaseDriverTest.java index 4edd5d449ba..767258e19a1 100644 --- a/modules/jdbc/src/test/java/org/testcontainers/jdbc/ContainerDatabaseDriverTest.java +++ b/modules/jdbc/src/test/java/org/testcontainers/jdbc/ContainerDatabaseDriverTest.java @@ -1,9 +1,6 @@ package org.testcontainers.jdbc; -import org.hamcrest.CoreMatchers; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; +import org.junit.jupiter.api.Test; import java.sql.Connection; import java.sql.DriverManager; @@ -11,25 +8,23 @@ import java.util.Properties; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; -public class ContainerDatabaseDriverTest { +class ContainerDatabaseDriverTest { private static final String PLAIN_POSTGRESQL_JDBC_URL = "jdbc:postgresql://localhost:5432/test"; - @Rule - public ExpectedException thrown = ExpectedException.none(); - @Test - public void shouldNotTryToConnectToNonMatchingJdbcUrlDirectly() throws SQLException { + void shouldNotTryToConnectToNonMatchingJdbcUrlDirectly() throws SQLException { ContainerDatabaseDriver driver = new ContainerDatabaseDriver(); Connection connection = driver.connect(PLAIN_POSTGRESQL_JDBC_URL, new Properties()); assertThat(connection).isNull(); } @Test - public void shouldNotTryToConnectToNonMatchingJdbcUrlViaDriverManager() throws SQLException { - thrown.expect(SQLException.class); - thrown.expectMessage(CoreMatchers.startsWith("No suitable driver found for ")); - DriverManager.getConnection(PLAIN_POSTGRESQL_JDBC_URL); + void shouldNotTryToConnectToNonMatchingJdbcUrlViaDriverManager() throws SQLException { + assertThatThrownBy(() -> DriverManager.getConnection(PLAIN_POSTGRESQL_JDBC_URL)) + .isInstanceOf(SQLException.class) + .hasMessageStartingWith("No suitable driver found for "); } } diff --git a/modules/jdbc/src/test/java/org/testcontainers/jdbc/JdbcDatabaseDelegateTest.java b/modules/jdbc/src/test/java/org/testcontainers/jdbc/JdbcDatabaseDelegateTest.java new file mode 100644 index 00000000000..e6233e14b9f --- /dev/null +++ b/modules/jdbc/src/test/java/org/testcontainers/jdbc/JdbcDatabaseDelegateTest.java @@ -0,0 +1,87 @@ +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) {} + } +} diff --git a/modules/jdbc/src/test/java/org/testcontainers/jdbc/MissingJdbcDriverTest.java b/modules/jdbc/src/test/java/org/testcontainers/jdbc/MissingJdbcDriverTest.java index 9f6b354818d..469234e15a6 100644 --- a/modules/jdbc/src/test/java/org/testcontainers/jdbc/MissingJdbcDriverTest.java +++ b/modules/jdbc/src/test/java/org/testcontainers/jdbc/MissingJdbcDriverTest.java @@ -1,7 +1,7 @@ package org.testcontainers.jdbc; import com.google.common.base.Throwables; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.utility.DockerImageName; @@ -12,10 +12,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; -public class MissingJdbcDriverTest { +class MissingJdbcDriverTest { @Test - public void shouldFailFastIfNoDriverFound() { + void shouldFailFastIfNoDriverFound() { final MissingDriverContainer container = new MissingDriverContainer(); try { diff --git a/modules/junit-jupiter/build.gradle b/modules/junit-jupiter/build.gradle index 95e0e7b1223..b9ac700cea4 100644 --- a/modules/junit-jupiter/build.gradle +++ b/modules/junit-jupiter/build.gradle @@ -2,27 +2,18 @@ description = "Testcontainers :: JUnit Jupiter Extension" dependencies { api project(':testcontainers') - implementation platform('org.junit:junit-bom:5.10.1') + implementation platform('org.junit:junit-bom:5.14.3') implementation 'org.junit.jupiter:junit-jupiter-api' - testImplementation project(':mysql') - testImplementation project(':postgresql') - testImplementation 'com.zaxxer:HikariCP:4.0.3' - testImplementation 'redis.clients:jedis:5.1.0' + testImplementation project(':testcontainers-mysql') + testImplementation project(':testcontainers-postgresql') + testImplementation 'com.zaxxer:HikariCP:7.0.2' + testImplementation 'redis.clients:jedis:7.5.3' testImplementation 'org.apache.httpcomponents:httpclient:4.5.14' testImplementation ('org.mockito:mockito-core:4.11.0') { exclude(module: 'hamcrest-core') } - testImplementation 'org.assertj:assertj-core:3.25.1' - testImplementation 'org.junit.jupiter:junit-jupiter' - testRuntimeOnly 'org.postgresql:postgresql:42.7.1' - testRuntimeOnly 'mysql:mysql-connector-java:8.0.33' -} - -test { - useJUnitPlatform() - testLogging { - events "passed", "skipped", "failed" - } + testRuntimeOnly 'org.postgresql:postgresql:42.7.12' + testRuntimeOnly 'com.mysql:mysql-connector-j:9.6.0' } diff --git a/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/DockerAvailableDetector.java b/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/DockerAvailableDetector.java new file mode 100644 index 00000000000..804568e7dfc --- /dev/null +++ b/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/DockerAvailableDetector.java @@ -0,0 +1,15 @@ +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; + } + } +} diff --git a/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/EnabledIfDockerAvailable.java b/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/EnabledIfDockerAvailable.java new file mode 100644 index 00000000000..6c78cbb7759 --- /dev/null +++ b/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/EnabledIfDockerAvailable.java @@ -0,0 +1,19 @@ +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 { +} diff --git a/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/EnabledIfDockerAvailableCondition.java b/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/EnabledIfDockerAvailableCondition.java new file mode 100644 index 00000000000..6067a9b4f4e --- /dev/null +++ b/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/EnabledIfDockerAvailableCondition.java @@ -0,0 +1,47 @@ +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(); + } +} diff --git a/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/TestcontainersExtension.java b/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/TestcontainersExtension.java index ca99a2d2382..89adba6033f 100644 --- a/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/TestcontainersExtension.java +++ b/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/TestcontainersExtension.java @@ -16,7 +16,6 @@ import org.junit.platform.commons.support.HierarchyTraversalMode; import org.junit.platform.commons.support.ModifierSupport; import org.junit.platform.commons.support.ReflectionSupport; -import org.testcontainers.DockerClientFactory; import org.testcontainers.lifecycle.Startable; import org.testcontainers.lifecycle.Startables; import org.testcontainers.lifecycle.TestDescription; @@ -42,6 +41,8 @@ public class TestcontainersExtension private static final String LOCAL_LIFECYCLE_AWARE_CONTAINERS = "localLifecycleAwareContainers"; + private final DockerAvailableDetector dockerDetector = new DockerAvailableDetector(); + @Override public void beforeAll(ExtensionContext context) { Class testClass = context @@ -192,12 +193,7 @@ private ConditionEvaluationResult evaluate(Testcontainers testcontainers) { } boolean isDockerAvailable() { - try { - DockerClientFactory.instance().client(); - return true; - } catch (Throwable ex) { - return false; - } + return this.dockerDetector.isDockerAvailable(); } private Set collectParentTestInstances(final ExtensionContext context) { @@ -264,7 +260,7 @@ private static StoreAdapter getContainerInstance(final Object testInstance, fina * thereby letting the JUnit automatically stop containers once the current * {@link ExtensionContext} is closed. */ - private static class StoreAdapter implements CloseableResource { + private static class StoreAdapter implements CloseableResource, AutoCloseable { @Getter private String key; diff --git a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/ComposeContainerTests.java b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/ComposeContainerTests.java index 866b76df301..a60b24a23d2 100644 --- a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/ComposeContainerTests.java +++ b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/ComposeContainerTests.java @@ -4,9 +4,8 @@ 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.BeforeEach; import org.junit.jupiter.api.Test; -import org.testcontainers.containers.DockerComposeContainer; +import org.testcontainers.containers.ComposeContainer; import org.testcontainers.containers.wait.strategy.Wait; import java.io.File; @@ -17,25 +16,16 @@ class ComposeContainerTests { @Container - private DockerComposeContainer composeContainer = new DockerComposeContainer( - new File("src/test/resources/docker-compose.yml") - ) - .withExposedService("whoami_1", 80, Wait.forHttp("/")); - - private String host; - - private int port; - - @BeforeEach - void setup() { - host = composeContainer.getServiceHost("whoami_1", 80); - port = composeContainer.getServicePort("whoami_1", 80); - } + 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); diff --git a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/DockerComposeContainerTests.java b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/DockerComposeContainerTests.java new file mode 100644 index 00000000000..460d50a856b --- /dev/null +++ b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/DockerComposeContainerTests.java @@ -0,0 +1,37 @@ +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); + } +} diff --git a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/EnabledIfDockerAvailableTests.java b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/EnabledIfDockerAvailableTests.java new file mode 100644 index 00000000000..455e94788f9 --- /dev/null +++ b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/EnabledIfDockerAvailableTests.java @@ -0,0 +1,48 @@ +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; + } + } +} diff --git a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/JUnitJupiterTestImages.java b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/JUnitJupiterTestImages.java index 9097c4d3457..4343b5dffc8 100644 --- a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/JUnitJupiterTestImages.java +++ b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/JUnitJupiterTestImages.java @@ -4,6 +4,8 @@ public interface JUnitJupiterTestImages { DockerImageName POSTGRES_IMAGE = DockerImageName.parse("postgres:9.6.12"); + DockerImageName HTTPD_IMAGE = DockerImageName.parse("httpd:2.4-alpine"); + DockerImageName MYSQL_IMAGE = DockerImageName.parse("mysql:8.0.32"); } diff --git a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/MixedLifecycleTests.java b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/MixedLifecycleTests.java index e8ce714ab28..7dcd769bf08 100644 --- a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/MixedLifecycleTests.java +++ b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/MixedLifecycleTests.java @@ -12,11 +12,11 @@ class MixedLifecycleTests { // will be shared between test methods @Container - private static final MySQLContainer MY_SQL_CONTAINER = new MySQLContainer(); + 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() + private PostgreSQLContainer postgresqlContainer = new PostgreSQLContainer("postgres:9.6.12") .withDatabaseName("foo") .withUsername("foo") .withPassword("secret"); diff --git a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/TestLifecycleAwareExceptionCapturingTest.java b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/TestLifecycleAwareExceptionCapturingTest.java index b64f2f3c328..a6647aa3e52 100644 --- a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/TestLifecycleAwareExceptionCapturingTest.java +++ b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/TestLifecycleAwareExceptionCapturingTest.java @@ -1,13 +1,13 @@ package org.testcontainers.junit.jupiter; -import org.junit.AssumptionViolatedException; 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.junit.Assume.assumeTrue; +import static org.assertj.core.api.Assumptions.assumeThat; // The order of @ExtendsWith and @Testcontainers is crucial in order for the tests @Testcontainers @@ -24,14 +24,14 @@ class TestLifecycleAwareExceptionCapturingTest { 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 - assumeTrue(false); + assumeThat(false).isTrue(); } @Test @Order(2) void should_have_captured_thrownException() { Throwable capturedThrowable = startedTestContainer.getCapturedThrowable(); - assertThat(capturedThrowable).isInstanceOf(AssumptionViolatedException.class); - assertThat(capturedThrowable.getMessage()).isEqualTo("got: , expected: is "); + assertThat(capturedThrowable).isInstanceOf(TestAbortedException.class); + assertThat(capturedThrowable.getMessage()).contains("Expecting value to be true but was false"); } } diff --git a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/inheritance/RedisContainer.java b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/inheritance/RedisContainer.java index 0a9ec62fca2..f6ed39c483e 100644 --- a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/inheritance/RedisContainer.java +++ b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/inheritance/RedisContainer.java @@ -7,7 +7,7 @@ public class RedisContainer extends GenericContainer { public RedisContainer() { - super(DockerImageName.parse("redis:3.2.11")); + super(DockerImageName.parse("redis:6-alpine")); withExposedPorts(6379); } diff --git a/modules/k3s/build.gradle b/modules/k3s/build.gradle index 70c8124fa6d..0a8d628b469 100644 --- a/modules/k3s/build.gradle +++ b/modules/k3s/build.gradle @@ -3,12 +3,9 @@ description = "Testcontainers :: K3S" dependencies { api project(":testcontainers") - // https://youtu.be/otCpCn0l4Wo - // The core module depends on jackson-databind 2.8.x for backward compatibility. - // Any >2.8 version here is not compatible with jackson-databind 2.8.x. - shaded 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.8.8' + // 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:6.10.0' - testImplementation 'io.kubernetes:client-java:19.0.0' - testImplementation 'org.assertj:assertj-core:3.25.2' + testImplementation 'io.fabric8:kubernetes-client:7.8.0' + testImplementation 'io.kubernetes:client-java:25.0.0-legacy' } diff --git a/modules/k3s/src/main/java/org/testcontainers/k3s/K3sContainer.java b/modules/k3s/src/main/java/org/testcontainers/k3s/K3sContainer.java index b014c5b4ed6..6dd6f06d85e 100644 --- a/modules/k3s/src/main/java/org/testcontainers/k3s/K3sContainer.java +++ b/modules/k3s/src/main/java/org/testcontainers/k3s/K3sContainer.java @@ -10,7 +10,7 @@ import org.apache.commons.io.IOUtils; import org.testcontainers.containers.BindMode; import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; +import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import java.nio.charset.StandardCharsets; @@ -47,7 +47,7 @@ public K3sContainer(DockerImageName dockerImageName) { setTmpFsMapping(tmpFsMapping); setCommand("server", "--disable=traefik", "--tls-san=" + this.getHost()); - setWaitStrategy(new LogMessageWaitStrategy().withRegEx(".*Node controller sync successful.*")); + setWaitStrategy(Wait.forLogMessage(".*Node controller sync successful.*", 1)); } @Override diff --git a/modules/k3s/src/test/java/org/testcontainers/k3s/Fabric8K3sContainerTest.java b/modules/k3s/src/test/java/org/testcontainers/k3s/Fabric8K3sContainerTest.java index 42b08444555..50b2e17f1fc 100644 --- a/modules/k3s/src/test/java/org/testcontainers/k3s/Fabric8K3sContainerTest.java +++ b/modules/k3s/src/test/java/org/testcontainers/k3s/Fabric8K3sContainerTest.java @@ -12,7 +12,7 @@ import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.dsl.Resource; import lombok.extern.slf4j.Slf4j; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.output.Slf4jLogConsumer; import org.testcontainers.utility.DockerImageName; @@ -22,10 +22,10 @@ import static org.assertj.core.api.Assertions.assertThat; @Slf4j -public class Fabric8K3sContainerTest { +class Fabric8K3sContainerTest { @Test - public void shouldStartAndHaveListableNode() { + void shouldStartAndHaveListableNode() { try ( // starting_k3s { K3sContainer k3s = new K3sContainer(DockerImageName.parse("rancher/k3s:v1.21.3-k3s1")) diff --git a/modules/k3s/src/test/java/org/testcontainers/k3s/KubectlContainerTest.java b/modules/k3s/src/test/java/org/testcontainers/k3s/KubectlContainerTest.java index 5c0d44ff0f1..6dcd7421363 100644 --- a/modules/k3s/src/test/java/org/testcontainers/k3s/KubectlContainerTest.java +++ b/modules/k3s/src/test/java/org/testcontainers/k3s/KubectlContainerTest.java @@ -1,7 +1,8 @@ package org.testcontainers.k3s; -import org.junit.ClassRule; -import org.junit.Test; +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; @@ -11,16 +12,26 @@ import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; -public class KubectlContainerTest { +class KubectlContainerTest { - public static Network network = Network.SHARED; + private static final Network network = Network.SHARED; - @ClassRule - public static K3sContainer k3s = new K3sContainer(DockerImageName.parse("rancher/k3s:v1.21.3-k3s1")) + 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"); @@ -38,8 +49,9 @@ public void shouldExposeKubeConfigForNetworkAlias() throws Exception { } } - @Test(expected = IllegalArgumentException.class) + @Test public void shouldThrowAnExceptionForUnknownNetworkAlias() { - k3s.generateInternalKubeConfigYaml("not-set-network-alias"); + assertThatThrownBy(() -> k3s.generateInternalKubeConfigYaml("not-set-network-alias")) + .isInstanceOf(IllegalArgumentException.class); } } diff --git a/modules/k3s/src/test/java/org/testcontainers/k3s/OfficialClientK3sContainerTest.java b/modules/k3s/src/test/java/org/testcontainers/k3s/OfficialClientK3sContainerTest.java index 1c40e56fc2d..f71cbae558b 100644 --- a/modules/k3s/src/test/java/org/testcontainers/k3s/OfficialClientK3sContainerTest.java +++ b/modules/k3s/src/test/java/org/testcontainers/k3s/OfficialClientK3sContainerTest.java @@ -6,7 +6,7 @@ import io.kubernetes.client.openapi.models.V1NodeList; import io.kubernetes.client.util.Config; import lombok.extern.slf4j.Slf4j; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.output.Slf4jLogConsumer; import org.testcontainers.utility.DockerImageName; @@ -16,15 +16,15 @@ import static org.assertj.core.api.Assertions.assertThat; @Slf4j -public class OfficialClientK3sContainerTest { +class OfficialClientK3sContainerTest { @Test - public void shouldStartAndHaveListableNode() throws IOException, ApiException { + void shouldStartAndHaveListableNode() throws IOException, ApiException { runK3s(DockerImageName.parse("rancher/k3s:v1.21.3-k3s1")); } @Test - public void shouldStartAndHaveListableNodeUsingLowerVersion() throws IOException, ApiException { + void shouldStartAndHaveListableNodeUsingLowerVersion() throws IOException, ApiException { runK3s(DockerImageName.parse("rancher/k3s:v1.20.15-k3s1")); } diff --git a/modules/k6/build.gradle b/modules/k6/build.gradle new file mode 100644 index 00000000000..0ca874ddc25 --- /dev/null +++ b/modules/k6/build.gradle @@ -0,0 +1,5 @@ +description = "Testcontainers :: k6" + +dependencies { + api project(':testcontainers') +} diff --git a/modules/k6/src/main/java/org/testcontainers/k6/K6Container.java b/modules/k6/src/main/java/org/testcontainers/k6/K6Container.java new file mode 100644 index 00000000000..04403ccb4cb --- /dev/null +++ b/modules/k6/src/main/java/org/testcontainers/k6/K6Container.java @@ -0,0 +1,88 @@ +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[] {})); + } +} diff --git a/modules/k6/src/test/java/org/testcontainers/k6/K6ContainerTests.java b/modules/k6/src/test/java/org/testcontainers/k6/K6ContainerTests.java new file mode 100644 index 00000000000..43b390c4db4 --- /dev/null +++ b/modules/k6/src/test/java/org/testcontainers/k6/K6ContainerTests.java @@ -0,0 +1,43 @@ +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!"); + } + } +} diff --git a/modules/k6/src/test/resources/logback-test.xml b/modules/k6/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..83ef7a1a3ef --- /dev/null +++ b/modules/k6/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + diff --git a/modules/k6/src/test/resources/scripts/test.js b/modules/k6/src/test/resources/scripts/test.js new file mode 100644 index 00000000000..fad0c4b48df --- /dev/null +++ b/modules/k6/src/test/resources/scripts/test.js @@ -0,0 +1,6 @@ +// access_script_vars { +// The most basic of k6 scripts. +export default function(){ + console.log(`k6 tests ${__ENV.MY_SCRIPT_VAR}`) +} +// } diff --git a/modules/kafka/build.gradle b/modules/kafka/build.gradle index 01e3551e651..aabad9553b0 100644 --- a/modules/kafka/build.gradle +++ b/modules/kafka/build.gradle @@ -3,7 +3,7 @@ description = "Testcontainers :: Kafka" dependencies { api project(':testcontainers') - testImplementation 'org.apache.kafka:kafka-clients:3.6.1' - testImplementation 'org.assertj:assertj-core:3.25.1' + testImplementation 'org.apache.kafka:kafka-clients:4.3.1' testImplementation 'com.google.guava:guava:23.0' + testImplementation 'org.awaitility:awaitility:4.3.0' } diff --git a/modules/kafka/src/main/java/org/testcontainers/containers/KafkaContainer.java b/modules/kafka/src/main/java/org/testcontainers/containers/KafkaContainer.java index d76154505c2..7eb836ade18 100644 --- a/modules/kafka/src/main/java/org/testcontainers/containers/KafkaContainer.java +++ b/modules/kafka/src/main/java/org/testcontainers/containers/KafkaContainer.java @@ -26,7 +26,11 @@ *
  • 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"); @@ -39,7 +43,7 @@ public class KafkaContainer extends GenericContainer { private static final String DEFAULT_INTERNAL_TOPIC_RF = "1"; - private static final String STARTER_SCRIPT = "/testcontainers_start.sh"; + 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"; @@ -182,15 +186,14 @@ protected void containerIsStarting(InspectContainerResponse containerInfo) { // exporting KAFKA_ADVERTISED_LISTENERS with the container hostname command += String.format("export KAFKA_ADVERTISED_LISTENERS=%s\n", kafkaAdvertisedListeners); - if (this.kraftEnabled && isLessThanCP740()) { + if (!this.kraftEnabled || isLessThanCP740()) { // Optimization: skip the checks command += "echo '' > /etc/confluent/docker/ensure \n"; - command += commandKraft(); } - if (!this.kraftEnabled) { - // Optimization: skip the checks - command += "echo '' > /etc/confluent/docker/ensure \n"; + if (this.kraftEnabled) { + command += commandKraft(); + } else if (this.externalZookeeperConnect == null) { command += commandZookeeper(); } @@ -209,10 +212,10 @@ protected String commandKraft() { } protected String commandZookeeper() { - String command = "echo 'clientPort=" + ZOOKEEPER_PORT + "' > zookeeper.properties\n"; - command += "echo 'dataDir=/var/lib/zookeeper/data' >> zookeeper.properties\n"; - command += "echo 'dataLogDir=/var/lib/zookeeper/log' >> zookeeper.properties\n"; - command += "zookeeper-server-start zookeeper.properties &\n"; + 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; } @@ -324,27 +327,34 @@ void withClusterId(String 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", - String.format("%s,CONTROLLER:PLAINTEXT", getEnvVars().get("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP")) - ); - addEnvVar( - "KAFKA_LISTENERS", - String.format("%s,CONTROLLER://0.0.0.0:9094", getEnvVars().get("KAFKA_LISTENERS")) - ); + addEnvVar("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", kafkaListenerSecurityProtocolMap()); + addEnvVar("KAFKA_LISTENERS", kafkaListeners()); addEnvVar("KAFKA_PROCESS_ROLES", "broker,controller"); - String firstNetworkAlias = getNetworkAliases().stream().findFirst().orElse(null); - String networkAlias = getNetwork() != null ? firstNetworkAlias : "localhost"; - String controllerQuorumVoters = String.format( - "%s@%s:9094", - getEnvVars().get("KAFKA_NODE_ID"), - networkAlias - ); + 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); + } } } diff --git a/modules/kafka/src/main/java/org/testcontainers/kafka/ConfluentKafkaContainer.java b/modules/kafka/src/main/java/org/testcontainers/kafka/ConfluentKafkaContainer.java new file mode 100644 index 00000000000..381ba836715 --- /dev/null +++ b/modules/kafka/src/main/java/org/testcontainers/kafka/ConfluentKafkaContainer.java @@ -0,0 +1,137 @@ +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)); + } +} diff --git a/modules/kafka/src/main/java/org/testcontainers/kafka/KafkaContainer.java b/modules/kafka/src/main/java/org/testcontainers/kafka/KafkaContainer.java new file mode 100644 index 00000000000..375fd132f6c --- /dev/null +++ b/modules/kafka/src/main/java/org/testcontainers/kafka/KafkaContainer.java @@ -0,0 +1,143 @@ +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)); + } +} diff --git a/modules/kafka/src/main/java/org/testcontainers/kafka/KafkaHelper.java b/modules/kafka/src/main/java/org/testcontainers/kafka/KafkaHelper.java new file mode 100644 index 00000000000..61e790d474f --- /dev/null +++ b/modules/kafka/src/main/java/org/testcontainers/kafka/KafkaHelper.java @@ -0,0 +1,108 @@ +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; + } +} diff --git a/modules/kafka/src/test/java/org/testcontainers/AbstractKafka.java b/modules/kafka/src/test/java/org/testcontainers/AbstractKafka.java new file mode 100644 index 00000000000..90977e61c18 --- /dev/null +++ b/modules/kafka/src/test/java/org/testcontainers/AbstractKafka.java @@ -0,0 +1,153 @@ +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; + } +} diff --git a/modules/kafka/src/test/java/org/testcontainers/KCatContainer.java b/modules/kafka/src/test/java/org/testcontainers/KCatContainer.java new file mode 100644 index 00000000000..79532ae9f22 --- /dev/null +++ b/modules/kafka/src/test/java/org/testcontainers/KCatContainer.java @@ -0,0 +1,16 @@ +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"); + } +} diff --git a/modules/kafka/src/test/java/org/testcontainers/containers/KafkaContainerTest.java b/modules/kafka/src/test/java/org/testcontainers/containers/KafkaContainerTest.java index e5d1617298b..e244d7a2e26 100644 --- a/modules/kafka/src/test/java/org/testcontainers/containers/KafkaContainerTest.java +++ b/modules/kafka/src/test/java/org/testcontainers/containers/KafkaContainerTest.java @@ -5,37 +5,26 @@ 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.errors.SaslAuthenticationException; import org.apache.kafka.common.errors.TopicAuthorizationException; -import org.apache.kafka.common.serialization.StringDeserializer; -import org.apache.kafka.common.serialization.StringSerializer; import org.awaitility.Awaitility; -import org.junit.Test; -import org.rnorth.ducttape.unreliables.Unreliables; +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.time.Duration; import java.util.Collection; import java.util.Collections; -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.assertThatThrownBy; -import static org.assertj.core.api.Assertions.tuple; -public class KafkaContainerTest { +class KafkaContainerTest extends AbstractKafka { private static final DockerImageName KAFKA_TEST_IMAGE = DockerImageName.parse("confluentinc/cp-kafka:6.2.1"); @@ -45,17 +34,8 @@ public class KafkaContainerTest { "confluentinc/cp-zookeeper:4.0.0" ); - private final ImmutableMap 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\";" - ); - @Test - public void testUsage() throws Exception { + void testUsage() throws Exception { try (KafkaContainer kafka = new KafkaContainer(KAFKA_TEST_IMAGE)) { kafka.start(); testKafkaFunctionality(kafka.getBootstrapServers()); @@ -63,7 +43,7 @@ public void testUsage() throws Exception { } @Test - public void testUsageWithSpecificImage() throws Exception { + void testUsageWithSpecificImage() throws Exception { try ( // constructorWithVersion { KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:6.2.1")) @@ -79,7 +59,7 @@ public void testUsageWithSpecificImage() throws Exception { } @Test - public void testUsageWithVersion() throws Exception { + void testUsageWithVersion() throws Exception { try (KafkaContainer kafka = new KafkaContainer("6.2.1")) { kafka.start(); testKafkaFunctionality(kafka.getBootstrapServers()); @@ -87,7 +67,7 @@ public void testUsageWithVersion() throws Exception { } @Test - public void testExternalZookeeperWithExternalNetwork() throws Exception { + void testExternalZookeeperWithExternalNetwork() throws Exception { try ( Network network = Network.newNetwork(); // withExternalZookeeper { @@ -109,7 +89,7 @@ public void testExternalZookeeperWithExternalNetwork() throws Exception { } @Test - public void testConfluentPlatformVersion7() throws Exception { + void testConfluentPlatformVersion7() throws Exception { try (KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.2.2"))) { kafka.start(); testKafkaFunctionality(kafka.getBootstrapServers()); @@ -117,7 +97,7 @@ public void testConfluentPlatformVersion7() throws Exception { } @Test - public void testConfluentPlatformVersion5() throws Exception { + void testConfluentPlatformVersion5() throws Exception { try (KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:5.4.3"))) { kafka.start(); testKafkaFunctionality(kafka.getBootstrapServers()); @@ -125,7 +105,7 @@ public void testConfluentPlatformVersion5() throws Exception { } @Test - public void testWithHostExposedPort() throws Exception { + void testWithHostExposedPort() throws Exception { Testcontainers.exposeHostPorts(12345); try (KafkaContainer kafka = new KafkaContainer(KAFKA_TEST_IMAGE)) { kafka.start(); @@ -134,7 +114,7 @@ public void testWithHostExposedPort() throws Exception { } @Test - public void testWithHostExposedPortAndExternalNetwork() throws Exception { + void testWithHostExposedPortAndExternalNetwork() throws Exception { Testcontainers.exposeHostPorts(12345); try (KafkaContainer kafka = new KafkaContainer(KAFKA_TEST_IMAGE).withNetwork(Network.newNetwork())) { kafka.start(); @@ -143,7 +123,7 @@ public void testWithHostExposedPortAndExternalNetwork() throws Exception { } @Test - public void testUsageKraftBeforeConfluentPlatformVersion74() throws Exception { + void testUsageKraftBeforeConfluentPlatformVersion74() throws Exception { try ( KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.0.1")).withKraft() ) { @@ -153,7 +133,7 @@ public void testUsageKraftBeforeConfluentPlatformVersion74() throws Exception { } @Test - public void testUsageKraftAfterConfluentPlatformVersion74() throws Exception { + void testUsageKraftAfterConfluentPlatformVersion74() throws Exception { try ( // withKraftMode { KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.4.0")).withKraft() @@ -165,7 +145,7 @@ public void testUsageKraftAfterConfluentPlatformVersion74() throws Exception { } @Test - public void testNotSupportedKraftVersion() { + void testNotSupportedKraftVersion() { try ( KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:6.2.1")).withKraft() ) {} catch (IllegalArgumentException e) { @@ -177,7 +157,7 @@ public void testNotSupportedKraftVersion() { } @Test - public void testKraftZookeeperMutualExclusion() { + void testKraftZookeeperMutualExclusion() { try ( KafkaContainer kafka = new KafkaContainer(KAFKA_KRAFT_TEST_IMAGE).withKraft().withExternalZookeeper("") ) {} catch (IllegalStateException e) { @@ -198,7 +178,7 @@ public void testKraftZookeeperMutualExclusion() { } @Test - public void testKraftPrecedenceOverEmbeddedZookeeper() throws Exception { + void testKraftPrecedenceOverEmbeddedZookeeper() throws Exception { try (KafkaContainer kafka = new KafkaContainer(KAFKA_KRAFT_TEST_IMAGE).withEmbeddedZookeeper().withKraft()) { kafka.start(); testKafkaFunctionality(kafka.getBootstrapServers()); @@ -206,16 +186,14 @@ public void testKraftPrecedenceOverEmbeddedZookeeper() throws Exception { } @Test - public void testUsageWithListener() throws Exception { + void testUsageWithListener() throws Exception { try ( Network network = Network.newNetwork(); - // registerListener { KafkaContainer kafka = new KafkaContainer(KAFKA_KRAFT_TEST_IMAGE) .withListener(() -> "kafka:19092") .withNetwork(network); - // } // createKCatContainer { - GenericContainer kcat = new GenericContainer<>("confluentinc/cp-kcat:7.4.1") + GenericContainer kcat = new GenericContainer<>("confluentinc/cp-kcat:7.9.0") .withCreateContainerCmdModifier(cmd -> { cmd.withEntrypoint("sh"); }) @@ -238,7 +216,7 @@ public void testUsageWithListener() throws Exception { @SneakyThrows @Test - public void shouldConfigureAuthenticationWithSaslUsingJaas() { + 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") @@ -250,13 +228,44 @@ public void shouldConfigureAuthenticationWithSaslUsingJaas() { ) { kafka.start(); - testSecureKafkaFunctionality(kafka.getBootstrapServers()); + 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 - public void enableSaslWithUnsuccessfulTopicCreation() { + 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") @@ -297,7 +306,7 @@ public void enableSaslWithUnsuccessfulTopicCreation() { @SneakyThrows @Test - public void enableSaslAndWithAuthenticationError() { + void enableSaslAndWithAuthenticationError() { String jaasConfig = getJaasConfig(); try ( KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:6.2.1")) @@ -334,101 +343,4 @@ public void enableSaslAndWithAuthenticationError() { }); } } - - private static String getJaasConfig() { - String jaasConfig = - "org.apache.kafka.common.security.plain.PlainLoginModule required " + - "username=\"admin\" " + - "password=\"admin\" " + - "user_admin=\"admin\" " + - "user_test=\"secret\";"; - return jaasConfig; - } - - private void testKafkaFunctionality(String bootstrapServers) throws Exception { - testKafkaFunctionality(bootstrapServers, false, 1, 1); - } - - private void testSecureKafkaFunctionality(String bootstrapServers) throws Exception { - testKafkaFunctionality(bootstrapServers, true, 1, 1); - } - - private void testKafkaFunctionality(String bootstrapServers, boolean authenticated, 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(this.properties); - consumerProperties.putAll(this.properties); - producerProperties.putAll(this.properties); - } - 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(); - - Unreliables.retryUntilTrue( - 10, - TimeUnit.SECONDS, - () -> { - ConsumerRecords records = consumer.poll(Duration.ofMillis(100)); - - if (records.isEmpty()) { - return false; - } - - assertThat(records) - .hasSize(1) - .extracting(ConsumerRecord::topic, ConsumerRecord::key, ConsumerRecord::value) - .containsExactly(tuple(topicName, "testcontainers", "rulezzz")); - - return true; - } - ); - - consumer.unsubscribe(); - } - } } diff --git a/modules/kafka/src/test/java/org/testcontainers/kafka/CompatibleApacheKafkaImageTest.java b/modules/kafka/src/test/java/org/testcontainers/kafka/CompatibleApacheKafkaImageTest.java new file mode 100644 index 00000000000..33916978acf --- /dev/null +++ b/modules/kafka/src/test/java/org/testcontainers/kafka/CompatibleApacheKafkaImageTest.java @@ -0,0 +1,21 @@ +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()); + } + } +} diff --git a/modules/kafka/src/test/java/org/testcontainers/kafka/ConfluentKafkaContainerTest.java b/modules/kafka/src/test/java/org/testcontainers/kafka/ConfluentKafkaContainerTest.java new file mode 100644 index 00000000000..1c95bf95e02 --- /dev/null +++ b/modules/kafka/src/test/java/org/testcontainers/kafka/ConfluentKafkaContainerTest.java @@ -0,0 +1,124 @@ +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()); + } + } +} diff --git a/modules/kafka/src/test/java/org/testcontainers/kafka/KafkaContainerTest.java b/modules/kafka/src/test/java/org/testcontainers/kafka/KafkaContainerTest.java new file mode 100644 index 00000000000..34af2e1784d --- /dev/null +++ b/modules/kafka/src/test/java/org/testcontainers/kafka/KafkaContainerTest.java @@ -0,0 +1,63 @@ +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); + } + } +} diff --git a/modules/kafka/src/test/resources/kafka_server_jaas.conf b/modules/kafka/src/test/resources/kafka_server_jaas.conf new file mode 100644 index 00000000000..89c88ed0522 --- /dev/null +++ b/modules/kafka/src/test/resources/kafka_server_jaas.conf @@ -0,0 +1,5 @@ +KafkaServer { + org.apache.kafka.common.security.scram.ScramLoginModule required + username="admin" + password="admin"; +}; diff --git a/modules/ldap/build.gradle b/modules/ldap/build.gradle new file mode 100644 index 00000000000..7e79be920c8 --- /dev/null +++ b/modules/ldap/build.gradle @@ -0,0 +1,7 @@ +description = "Testcontainers :: LDAP" + +dependencies { + api project(':testcontainers') + + testImplementation 'com.unboundid:unboundid-ldapsdk:7.0.5' +} diff --git a/modules/ldap/src/main/java/org/testcontainers/ldap/LLdapContainer.java b/modules/ldap/src/main/java/org/testcontainers/ldap/LLdapContainer.java new file mode 100644 index 00000000000..c811bffa1ec --- /dev/null +++ b/modules/ldap/src/main/java/org/testcontainers/ldap/LLdapContainer.java @@ -0,0 +1,90 @@ +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"); + } +} diff --git a/modules/ldap/src/test/java/org/testcontainers/ldap/LLdapContainerTest.java b/modules/ldap/src/test/java/org/testcontainers/ldap/LLdapContainerTest.java new file mode 100644 index 00000000000..96e77c9a3f0 --- /dev/null +++ b/modules/ldap/src/test/java/org/testcontainers/ldap/LLdapContainerTest.java @@ -0,0 +1,66 @@ +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(); + } + } +} diff --git a/modules/ldap/src/test/resources/logback-test.xml b/modules/ldap/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..83ef7a1a3ef --- /dev/null +++ b/modules/ldap/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + diff --git a/modules/localstack/build.gradle b/modules/localstack/build.gradle index 97c5ad9ce35..eca4aeaa27a 100644 --- a/modules/localstack/build.gradle +++ b/modules/localstack/build.gradle @@ -3,10 +3,10 @@ description = "Testcontainers :: Localstack" dependencies { api project(':testcontainers') - testImplementation platform("com.amazonaws:aws-java-sdk-bom:1.12.572") - testImplementation 'com.amazonaws:aws-java-sdk-s3' - testImplementation 'com.amazonaws:aws-java-sdk-sqs' - testImplementation 'com.amazonaws:aws-java-sdk-logs' - testImplementation 'software.amazon.awssdk:s3:2.23.9' - testImplementation 'org.assertj:assertj-core:3.25.2' + testImplementation platform("software.amazon.awssdk:bom:2.46.20") + 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' } diff --git a/modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java b/modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java index b3651aea9e5..ce50313d429 100644 --- a/modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java +++ b/modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java @@ -1,5 +1,6 @@ package org.testcontainers.containers.localstack; +import com.github.dockerjava.api.command.InspectContainerResponse; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; @@ -8,6 +9,7 @@ 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; @@ -26,8 +28,11 @@ * 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; @@ -51,6 +56,8 @@ public class LocalStackContainer extends GenericContainer { 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; @@ -119,6 +126,13 @@ public LocalStackContainer(final DockerImageName dockerImageName, boolean useLeg 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) { @@ -145,8 +159,10 @@ private static boolean isServicesEnvVarRequired(String version) { return true; } - private static boolean shouldRunInLegacyMode(String version) { - if (version.equals("latest")) { + 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; } @@ -187,6 +203,53 @@ protected void configure() { 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)) { diff --git a/modules/localstack/src/main/java/org/testcontainers/localstack/LocalStackContainer.java b/modules/localstack/src/main/java/org/testcontainers/localstack/LocalStackContainer.java new file mode 100644 index 00000000000..31eee4ca5ca --- /dev/null +++ b/modules/localstack/src/main/java/org/testcontainers/localstack/LocalStackContainer.java @@ -0,0 +1,220 @@ +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.getEndpoint())
    +             .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.getEndpoint())
    +             .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.getEndpoint())
    +             .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); + } +} diff --git a/modules/localstack/src/test/java/org/testcontainers/containers/localstack/LegacyModeTest.java b/modules/localstack/src/test/java/org/testcontainers/containers/localstack/LegacyModeTest.java index a535699438f..b2537d70b77 100644 --- a/modules/localstack/src/test/java/org/testcontainers/containers/localstack/LegacyModeTest.java +++ b/modules/localstack/src/test/java/org/testcontainers/containers/localstack/LegacyModeTest.java @@ -1,142 +1,118 @@ package org.testcontainers.containers.localstack; -import com.amazonaws.client.builder.AwsClientBuilder; import com.github.dockerjava.api.DockerClient; -import lombok.AllArgsConstructor; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.experimental.runners.Enclosed; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; +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.Arrays; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; -@RunWith(Enclosed.class) -public class LegacyModeTest { - - private static DockerImageName LOCALSTACK_CUSTOM_TAG = LocalstackTestImages.LOCALSTACK_IMAGE.withTag("custom"); - - @RunWith(Parameterized.class) - @AllArgsConstructor - public static class Off { - - private final String description; - - private final LocalStackContainer localstack; - - @Parameterized.Parameters(name = "{0}") - public static Iterable constructors() { - return Arrays.asList( - new Object[][] { - { "0.12", new LocalStackContainer(LocalstackTestImages.LOCALSTACK_0_12_IMAGE) }, - { "0.11", new LocalStackContainer(LocalstackTestImages.LOCALSTACK_0_11_IMAGE) }, - { - "0.7 with legacy = off", - new LocalStackContainer(LocalstackTestImages.LOCALSTACK_0_7_IMAGE, false), - }, - } - ); - } +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(); + } - @Test - public void samePortIsExposedForAllServices() { - localstack.withServices(Service.S3, Service.SQS); - localstack.start(); + 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) + ) + ); + } - 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( - new AwsClientBuilder.EndpointConfiguration( - localstack.getEndpointOverride(Service.SQS).toString(), - localstack.getRegion() - ) - .getServiceEndpoint() - ) - .as("Endpoint configuration have different endpoints") - .isEqualTo( - new AwsClientBuilder.EndpointConfiguration( - localstack.getEndpointOverride(Service.S3).toString(), - localstack.getRegion() - ) - .getServiceEndpoint() - ); - } finally { - localstack.stop(); - } + @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(); } } - @RunWith(Parameterized.class) - @AllArgsConstructor - public static class On { - - private final String description; - - private final LocalStackContainer localstack; - - @BeforeClass - public static void createCustomTag() { - DockerClient dockerClient = DockerClientFactory.instance().client(); - dockerClient - .tagImageCmd( - new RemoteDockerImage(LocalstackTestImages.LOCALSTACK_0_12_IMAGE).get(), - LOCALSTACK_CUSTOM_TAG.getRepository(), - LOCALSTACK_CUSTOM_TAG.getVersionPart() - ) - .exec(); - } + 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) + ) + ); + } - @Parameterized.Parameters(name = "{0}") - public static Iterable constructors() { - return Arrays.asList( - new Object[][] { - { "0.10", new LocalStackContainer(LocalstackTestImages.LOCALSTACK_0_10_IMAGE) }, - { "custom", new LocalStackContainer(LOCALSTACK_CUSTOM_TAG) }, - { - "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(); } + } - @Test - public void differentPortsAreExposed() { - localstack.withServices(Service.S3, Service.SQS); - localstack.start(); + 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) + ); + } - 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( - new AwsClientBuilder.EndpointConfiguration( - localstack.getEndpointOverride(Service.SQS).toString(), - localstack.getRegion() - ) - .getServiceEndpoint() - ) - .as("Endpoint configuration have different endpoints") - .isNotEqualTo( - new AwsClientBuilder.EndpointConfiguration( - localstack.getEndpointOverride(Service.S3).toString(), - localstack.getRegion() - ) - .getServiceEndpoint() - ); - } finally { - localstack.stop(); - } - } + @ParameterizedTest + @MethodSource("constructors") + void testLegacyMode(String version, boolean shouldUseLegacyMode) { + assertThat(LocalStackContainer.shouldRunInLegacyMode(version)).isEqualTo(shouldUseLegacyMode); } } diff --git a/modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackContainerTest.java b/modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackContainerTest.java deleted file mode 100644 index ef0c9b728c0..00000000000 --- a/modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackContainerTest.java +++ /dev/null @@ -1,508 +0,0 @@ -package org.testcontainers.containers.localstack; - -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.client.builder.AwsClientBuilder; -import com.amazonaws.services.kms.AWSKMS; -import com.amazonaws.services.kms.AWSKMSClientBuilder; -import com.amazonaws.services.kms.model.CreateKeyRequest; -import com.amazonaws.services.kms.model.CreateKeyResult; -import com.amazonaws.services.kms.model.Tag; -import com.amazonaws.services.logs.AWSLogs; -import com.amazonaws.services.logs.AWSLogsClientBuilder; -import com.amazonaws.services.logs.model.CreateLogGroupRequest; -import com.amazonaws.services.logs.model.LogGroup; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; -import com.amazonaws.services.s3.model.Bucket; -import com.amazonaws.services.s3.model.ObjectListing; -import com.amazonaws.services.s3.model.S3Object; -import com.amazonaws.services.sqs.AmazonSQS; -import com.amazonaws.services.sqs.AmazonSQSClientBuilder; -import com.amazonaws.services.sqs.model.CreateQueueResult; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.io.IOUtils; -import org.junit.ClassRule; -import org.junit.Test; -import org.junit.experimental.runners.Enclosed; -import org.junit.runner.RunWith; -import org.testcontainers.containers.Container; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.Network; -import org.testcontainers.containers.localstack.LocalStackContainer.Service; -import org.testcontainers.utility.DockerImageName; -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.s3.S3Client; - -import java.io.IOException; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Date; -import java.util.List; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for Localstack Container, used both in bridge network (exposed to host) and docker network modes. - *

    - * These tests attempt simple interactions with the container to verify behaviour. The bridge network tests use the - * Java AWS SDK, whereas the docker network tests use an AWS CLI container within the network, to simulate usage of - * Localstack from within a Docker network. - */ -@Slf4j -@RunWith(Enclosed.class) -public class LocalstackContainerTest { - - public static class WithoutNetwork { - - // without_network { - @ClassRule - public static LocalStackContainer localstack = new LocalStackContainer(LocalstackTestImages.LOCALSTACK_IMAGE) - .withServices( - Service.S3, - Service.SQS, - Service.CLOUDWATCHLOGS, - Service.KMS, - LocalStackContainer.EnabledService.named("events") - ); - - // } - - @Test - public void s3TestOverBridgeNetwork() throws IOException { - // with_aws_sdk_v1 { - AmazonS3 s3 = AmazonS3ClientBuilder - .standard() - .withEndpointConfiguration( - new AwsClientBuilder.EndpointConfiguration( - localstack.getEndpoint().toString(), - localstack.getRegion() - ) - ) - .withCredentials( - new AWSStaticCredentialsProvider( - new BasicAWSCredentials(localstack.getAccessKey(), localstack.getSecretKey()) - ) - ) - .build(); - // } - - final String bucketName = "foo"; - s3.createBucket(bucketName); - s3.putObject(bucketName, "bar", "baz"); - - final List buckets = s3.listBuckets(); - final Optional maybeBucket = buckets - .stream() - .filter(b -> b.getName().equals(bucketName)) - .findFirst(); - assertThat(maybeBucket).as("The created bucket is present").isPresent(); - final Bucket bucket = maybeBucket.get(); - - assertThat(bucket.getName()).as("The created bucket has the right name").isEqualTo(bucketName); - - final ObjectListing objectListing = s3.listObjects(bucketName); - assertThat(objectListing.getObjectSummaries()).as("The created bucket has 1 item in it").hasSize(1); - - final S3Object object = s3.getObject(bucketName, "bar"); - final String content = IOUtils.toString(object.getObjectContent(), StandardCharsets.UTF_8); - assertThat(content).as("The object can be retrieved").isEqualTo("baz"); - } - - @Test - public void s3TestUsingAwsSdkV2() { - // 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 = "foov2"; - s3.createBucket(b -> b.bucket(bucketName)); - assertThat(s3.listBuckets().buckets().stream().anyMatch(b -> b.name().equals(bucketName))) - .as("New bucket was created") - .isTrue(); - } - - @Test - public void sqsTestOverBridgeNetwork() { - AmazonSQS sqs = AmazonSQSClientBuilder - .standard() - .withEndpointConfiguration( - new AwsClientBuilder.EndpointConfiguration( - localstack.getEndpoint().toString(), - localstack.getRegion() - ) - ) - .withCredentials( - new AWSStaticCredentialsProvider( - new BasicAWSCredentials(localstack.getAccessKey(), localstack.getSecretKey()) - ) - ) - .build(); - - CreateQueueResult queueResult = sqs.createQueue("baz"); - String fooQueueUrl = queueResult.getQueueUrl(); - assertThat(fooQueueUrl) - .as("Created queue has external hostname URL") - .contains("http://" + localstack.getHost() + ":" + localstack.getMappedPort(LocalStackContainer.PORT)); - - sqs.sendMessage(fooQueueUrl, "test"); - final long messageCount = sqs - .receiveMessage(fooQueueUrl) - .getMessages() - .stream() - .filter(message -> message.getBody().equals("test")) - .count(); - assertThat(messageCount).as("the sent message can be received").isEqualTo(1L); - } - - @Test - public void cloudWatchLogsTestOverBridgeNetwork() { - AWSLogs logs = AWSLogsClientBuilder - .standard() - .withEndpointConfiguration( - new AwsClientBuilder.EndpointConfiguration( - localstack.getEndpoint().toString(), - localstack.getRegion() - ) - ) - .withCredentials( - new AWSStaticCredentialsProvider( - new BasicAWSCredentials(localstack.getAccessKey(), localstack.getSecretKey()) - ) - ) - .build(); - - logs.createLogGroup(new CreateLogGroupRequest("foo")); - - List groups = logs.describeLogGroups().getLogGroups(); - assertThat(groups).as("One log group should be created").hasSize(1); - assertThat(groups.get(0).getLogGroupName()).as("Name of created log group is [foo]").isEqualTo("foo"); - } - - @Test - public void kmsKeyCreationTest() { - AWSKMS awskms = AWSKMSClientBuilder - .standard() - .withEndpointConfiguration( - new AwsClientBuilder.EndpointConfiguration( - localstack.getEndpoint().toString(), - localstack.getRegion() - ) - ) - .withCredentials( - new AWSStaticCredentialsProvider( - new BasicAWSCredentials(localstack.getAccessKey(), localstack.getSecretKey()) - ) - ) - .build(); - - String desc = String.format("AWS CMK Description"); - Tag createdByTag = new Tag().withTagKey("CreatedBy").withTagValue("StorageService"); - CreateKeyRequest req = new CreateKeyRequest().withDescription(desc).withTags(createdByTag); - CreateKeyResult key = awskms.createKey(req); - - assertThat(desc) - .as("AWS KMS Customer Managed Key should be created ") - .isEqualTo(key.getKeyMetadata().getDescription()); - } - - @Test - public void samePortIsExposedForAllServices() { - 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( - new AwsClientBuilder.EndpointConfiguration( - localstack.getEndpointOverride(Service.SQS).toString(), - localstack.getRegion() - ) - .getServiceEndpoint() - ) - .as("Endpoint configuration have different endpoints") - .isEqualTo( - new AwsClientBuilder.EndpointConfiguration( - localstack.getEndpointOverride(Service.S3).toString(), - localstack.getRegion() - ) - .getServiceEndpoint() - ); - } - } - - public static class WithNetwork { - - // with_network { - private static Network network = Network.newNetwork(); - - @ClassRule - public static LocalStackContainer localstackInDockerNetwork = new LocalStackContainer( - LocalstackTestImages.LOCALSTACK_IMAGE - ) - .withNetwork(network) - .withNetworkAliases("notthis", "localstack") // the last alias is used for HOSTNAME_EXTERNAL - .withServices(Service.S3, Service.SQS, Service.CLOUDWATCHLOGS); - - // } - - @ClassRule - public static 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"); - - @Test - public void localstackHostEnVarIsSet() { - assertThat(localstackInDockerNetwork.getEnvMap().get("HOSTNAME_EXTERNAL")).isEqualTo("localstack"); - } - - @Test - public 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 - public void sqsTestOverDockerNetwork() throws Exception { - final String queueCreationResponse = runAwsCliAgainstDockerNetworkContainer( - "sqs create-queue --queue-name baz" - ); - - assertThat(queueCreationResponse) - .as("Created queue has external hostname URL") - .contains("http://localstack:" + LocalStackContainer.PORT); - - runAwsCliAgainstDockerNetworkContainer( - String.format( - "sqs send-message --endpoint http://localstack:%d --queue-url http://localstack:%d/queue/baz --message-body test", - LocalStackContainer.PORT, - LocalStackContainer.PORT - ) - ); - final String message = runAwsCliAgainstDockerNetworkContainer( - String.format( - "sqs receive-message --endpoint http://localstack:%d --queue-url http://localstack:%d/queue/baz", - LocalStackContainer.PORT, - LocalStackContainer.PORT - ) - ); - - assertThat(message).as("the sent message can be received").contains("\"Body\": \"test\""); - } - - @Test - public 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; - } - } - - public static class WithRegion { - - // with_region { - private static String region = "eu-west-1"; - - @ClassRule - public static LocalStackContainer localstack = new LocalStackContainer(LocalstackTestImages.LOCALSTACK_IMAGE) - .withEnv("DEFAULT_REGION", region) - .withServices(Service.S3); - - // } - - @Test - public void s3EndpointHasProperRegion() { - final AwsClientBuilder.EndpointConfiguration endpointConfiguration = new AwsClientBuilder.EndpointConfiguration( - localstack.getEndpoint().toString(), - localstack.getRegion() - ); - assertThat(endpointConfiguration.getSigningRegion()) - .as("The endpoint configuration has right region") - .isEqualTo(region); - } - } - - public static class WithoutServices { - - @ClassRule - public static LocalStackContainer localstack = new LocalStackContainer( - LocalstackTestImages.LOCALSTACK_0_13_IMAGE - ); - - @Test - public void s3ServiceStartLazily() { - try ( - 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(); - } - } - } - - public static class WithVersion2 { - - private static Network network = Network.newNetwork(); - - @ClassRule - public static LocalStackContainer localstack = new LocalStackContainer( - DockerImageName.parse("localstack/localstack:2.0") - ) - .withNetwork(network) - .withNetworkAliases("localstack"); - - @ClassRule - public static 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"); - - @Test - public void localstackHostEnVarIsSet() { - assertThat(localstack.getEnvMap().get("LOCALSTACK_HOST")).isEqualTo("localstack"); - } - - @Test - public void sqsTestOverDockerNetwork() throws Exception { - final String queueCreationResponse = runAwsCliAgainstDockerNetworkContainer( - "sqs create-queue --queue-name baz" - ); - - assertThat(queueCreationResponse) - .as("Created queue has external hostname URL") - .contains("http://localstack:" + LocalStackContainer.PORT); - - runAwsCliAgainstDockerNetworkContainer( - String.format( - "sqs send-message --endpoint http://localstack:%d --queue-url http://localstack:%d/queue/baz --message-body test", - LocalStackContainer.PORT, - LocalStackContainer.PORT - ) - ); - final String message = runAwsCliAgainstDockerNetworkContainer( - String.format( - "sqs receive-message --endpoint http://localstack:%d --queue-url http://localstack:%d/queue/baz", - LocalStackContainer.PORT, - LocalStackContainer.PORT - ) - ); - - assertThat(message).as("the sent message can be received").contains("\"Body\": \"test\""); - } - - 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; - } - } - - public static class S3SkipSignatureValidation { - - @ClassRule - public static LocalStackContainer localstack = new LocalStackContainer( - LocalstackTestImages.LOCALSTACK_2_3_IMAGE - ) - .withEnv("S3_SKIP_SIGNATURE_VALIDATION", "0"); - - @Test - public void shouldBeAccessibleWithCredentials() throws IOException { - AmazonS3 s3 = AmazonS3ClientBuilder - .standard() - .withEndpointConfiguration( - new AwsClientBuilder.EndpointConfiguration( - localstack.getEndpoint().toString(), - localstack.getRegion() - ) - ) - .withCredentials( - new AWSStaticCredentialsProvider( - new BasicAWSCredentials(localstack.getAccessKey(), localstack.getSecretKey()) - ) - ) - .build(); - - final String bucketName = "foo"; - - s3.createBucket(bucketName); - - s3.putObject(bucketName, "bar", "baz"); - - final List buckets = s3.listBuckets(); - final Optional maybeBucket = buckets - .stream() - .filter(b -> b.getName().equals(bucketName)) - .findFirst(); - assertThat(maybeBucket).as("The created bucket is present").isPresent(); - - URL presignedUrl = s3.generatePresignedUrl( - bucketName, - "bar", - Date.from(Instant.now().plus(5, ChronoUnit.MINUTES)) - ); - - 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"); - } - } -} diff --git a/modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackTestImages.java b/modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackTestImages.java index 0a9be877f4d..9ca7dd6614f 100644 --- a/modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackTestImages.java +++ b/modules/localstack/src/test/java/org/testcontainers/containers/localstack/LocalstackTestImages.java @@ -3,13 +3,13 @@ import org.testcontainers.utility.DockerImageName; public interface LocalstackTestImages { - DockerImageName LOCALSTACK_IMAGE = DockerImageName.parse("localstack/localstack:0.12.8"); - DockerImageName LOCALSTACK_0_7_IMAGE = LOCALSTACK_IMAGE.withTag("0.7.0"); + 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 LOCALSTACK_0_13_IMAGE = LOCALSTACK_IMAGE.withTag("0.13.0"); - DockerImageName LOCALSTACK_2_3_IMAGE = LOCALSTACK_IMAGE.withTag("2.3"); DockerImageName AWS_CLI_IMAGE = DockerImageName.parse("amazon/aws-cli:2.7.27"); } diff --git a/modules/localstack/src/test/java/org/testcontainers/localstack/LocalStackContainerTest.java b/modules/localstack/src/test/java/org/testcontainers/localstack/LocalStackContainerTest.java new file mode 100644 index 00000000000..f3fa6a07457 --- /dev/null +++ b/modules/localstack/src/test/java/org/testcontainers/localstack/LocalStackContainerTest.java @@ -0,0 +1,512 @@ +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(); + } + } + } +} diff --git a/modules/mariadb/build.gradle b/modules/mariadb/build.gradle index e3f33b0ca8a..10086b4e058 100644 --- a/modules/mariadb/build.gradle +++ b/modules/mariadb/build.gradle @@ -1,17 +1,14 @@ description = "Testcontainers :: JDBC :: MariaDB" dependencies { - annotationProcessor 'com.google.auto.service:auto-service:1.1.1' - compileOnly 'com.google.auto.service:auto-service:1.1.1' + api project(':testcontainers-jdbc') - api project(':jdbc') - - compileOnly project(':r2dbc') + compileOnly project(':testcontainers-r2dbc') compileOnly 'org.mariadb:r2dbc-mariadb:1.0.3' - testImplementation project(':jdbc-test') - testImplementation 'org.mariadb.jdbc:mariadb-java-client:3.3.2' + testImplementation project(':testcontainers-jdbc-test') + testImplementation 'org.mariadb.jdbc:mariadb-java-client:3.5.9' - testImplementation testFixtures(project(':r2dbc')) + testImplementation testFixtures(project(':testcontainers-r2dbc')) testRuntimeOnly 'org.mariadb:r2dbc-mariadb:1.0.3' } diff --git a/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBContainer.java b/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBContainer.java index 1273ac4ec28..6c9ffda7722 100644 --- a/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBContainer.java +++ b/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBContainer.java @@ -12,7 +12,10 @@ * 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"); @@ -41,14 +44,6 @@ public class MariaDBContainer> extends JdbcD private static final String MY_CNF_CONFIG_OVERRIDE_PARAM_NAME = "TC_MY_CNF"; - /** - * @deprecated use {@link #MariaDBContainer(DockerImageName)} instead - */ - @Deprecated - public MariaDBContainer() { - this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); - } - public MariaDBContainer(String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } @@ -75,7 +70,10 @@ protected void configure() { ); addEnv("MYSQL_DATABASE", databaseName); - addEnv("MYSQL_USER", username); + + 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); diff --git a/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerProvider.java b/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerProvider.java index 6994ab44dff..34635ac239f 100644 --- a/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerProvider.java +++ b/modules/mariadb/src/main/java/org/testcontainers/containers/MariaDBR2DBCDatabaseContainerProvider.java @@ -1,6 +1,5 @@ package org.testcontainers.containers; -import com.google.auto.service.AutoService; import io.r2dbc.spi.ConnectionFactoryMetadata; import io.r2dbc.spi.ConnectionFactoryOptions; import org.mariadb.r2dbc.MariadbConnectionFactoryProvider; @@ -9,7 +8,6 @@ import javax.annotation.Nullable; -@AutoService(R2DBCDatabaseContainerProvider.class) public class MariaDBR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider { static final String DRIVER = MariadbConnectionFactoryProvider.MARIADB_DRIVER; diff --git a/modules/mariadb/src/main/java/org/testcontainers/mariadb/MariaDBContainer.java b/modules/mariadb/src/main/java/org/testcontainers/mariadb/MariaDBContainer.java new file mode 100644 index 00000000000..8487f4576f7 --- /dev/null +++ b/modules/mariadb/src/main/java/org/testcontainers/mariadb/MariaDBContainer.java @@ -0,0 +1,136 @@ +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(); + } +} diff --git a/modules/mariadb/src/main/java/org/testcontainers/mariadb/MariaDBR2DBCDatabaseContainer.java b/modules/mariadb/src/main/java/org/testcontainers/mariadb/MariaDBR2DBCDatabaseContainer.java new file mode 100644 index 00000000000..4c9e6350d23 --- /dev/null +++ b/modules/mariadb/src/main/java/org/testcontainers/mariadb/MariaDBR2DBCDatabaseContainer.java @@ -0,0 +1,58 @@ +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(); + } +} diff --git a/modules/mariadb/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider b/modules/mariadb/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider new file mode 100644 index 00000000000..0c312685de9 --- /dev/null +++ b/modules/mariadb/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider @@ -0,0 +1 @@ +org.testcontainers.containers.MariaDBR2DBCDatabaseContainerProvider diff --git a/modules/mariadb/src/test/java/org/testcontainers/jdbc/mariadb/MariaDBJDBCDriverTest.java b/modules/mariadb/src/test/java/org/testcontainers/jdbc/mariadb/MariaDBJDBCDriverTest.java index 9d903ab3965..5d3b799f0ff 100644 --- a/modules/mariadb/src/test/java/org/testcontainers/jdbc/mariadb/MariaDBJDBCDriverTest.java +++ b/modules/mariadb/src/test/java/org/testcontainers/jdbc/mariadb/MariaDBJDBCDriverTest.java @@ -1,16 +1,12 @@ package org.testcontainers.jdbc.mariadb; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; -@RunWith(Parameterized.class) -public class MariaDBJDBCDriverTest extends AbstractJDBCDriverTest { +class MariaDBJDBCDriverTest extends AbstractJDBCDriverTest { - @Parameterized.Parameters(name = "{index} - {0}") public static Iterable data() { return Arrays.asList( new Object[][] { diff --git a/modules/mariadb/src/test/java/org/testcontainers/junit/mariadb/SimpleMariaDBTest.java b/modules/mariadb/src/test/java/org/testcontainers/mariadb/MariaDBContainerTest.java similarity index 71% rename from modules/mariadb/src/test/java/org/testcontainers/junit/mariadb/SimpleMariaDBTest.java rename to modules/mariadb/src/test/java/org/testcontainers/mariadb/MariaDBContainerTest.java index 1b6996f5478..99348f9168d 100644 --- a/modules/mariadb/src/test/java/org/testcontainers/junit/mariadb/SimpleMariaDBTest.java +++ b/modules/mariadb/src/test/java/org/testcontainers/mariadb/MariaDBContainerTest.java @@ -1,9 +1,8 @@ -package org.testcontainers.junit.mariadb; +package org.testcontainers.mariadb; import org.apache.commons.lang3.SystemUtils; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.MariaDBTestImages; -import org.testcontainers.containers.MariaDBContainer; import org.testcontainers.db.AbstractContainerDatabaseTest; import java.io.File; @@ -19,13 +18,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assumptions.assumeThat; -import static org.junit.Assume.assumeFalse; -public class SimpleMariaDBTest extends AbstractContainerDatabaseTest { +class MariaDBContainerTest extends AbstractContainerDatabaseTest { @Test - public void testSimple() throws SQLException { - try (MariaDBContainer mariadb = new MariaDBContainer<>(MariaDBTestImages.MARIADB_IMAGE)) { + void testSimple() throws SQLException { + try ( // container { + MariaDBContainer mariadb = new MariaDBContainer("mariadb:10.3.39") + // } + ) { mariadb.start(); ResultSet resultSet = performQuery(mariadb, "SELECT 1"); @@ -36,9 +37,9 @@ public void testSimple() throws SQLException { } @Test - public void testSpecificVersion() throws SQLException { + void testSpecificVersion() throws SQLException { try ( - MariaDBContainer mariadbOldVersion = new MariaDBContainer<>( + MariaDBContainer mariadbOldVersion = new MariaDBContainer( MariaDBTestImages.MARIADB_IMAGE.withTag("10.3.39") ) ) { @@ -54,11 +55,11 @@ public void testSpecificVersion() throws SQLException { } @Test - public void testMariaDBWithCustomIniFile() throws SQLException { - assumeFalse(SystemUtils.IS_OS_WINDOWS); + void testMariaDBWithCustomIniFile() throws SQLException { + assumeThat(SystemUtils.IS_OS_WINDOWS).isFalse(); try ( - MariaDBContainer mariadbCustomConfig = new MariaDBContainer<>( + MariaDBContainer mariadbCustomConfig = new MariaDBContainer( MariaDBTestImages.MARIADB_IMAGE.withTag("10.3.39") ) .withConfigurationOverride("somepath/mariadb_conf_override") @@ -70,22 +71,22 @@ public void testMariaDBWithCustomIniFile() throws SQLException { } @Test - public void testMariaDBWithCommandOverride() throws SQLException { + void testMariaDBWithCommandOverride() throws SQLException { try ( - MariaDBContainer mariadbCustomConfig = new MariaDBContainer<>(MariaDBTestImages.MARIADB_IMAGE) + 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 overriden by command line").isEqualTo("10"); + assertThat(result).as("Auto increment increment should be overridden by command line").isEqualTo("10"); } } @Test - public void testWithAdditionalUrlParamInJdbcUrl() { - MariaDBContainer mariaDBContainer = new MariaDBContainer<>(MariaDBTestImages.MARIADB_IMAGE) + void testWithAdditionalUrlParamInJdbcUrl() { + MariaDBContainer mariaDBContainer = new MariaDBContainer(MariaDBTestImages.MARIADB_IMAGE) .withUrlParam("connectTimeout", "40000") .withUrlParam("rewriteBatchedStatements", "true"); @@ -102,11 +103,11 @@ public void testWithAdditionalUrlParamInJdbcUrl() { } @Test - public void testWithOnlyUserReadableCustomIniFile() throws Exception { + void testWithOnlyUserReadableCustomIniFile() throws Exception { assumeThat(FileSystems.getDefault().supportedFileAttributeViews().contains("posix")).isTrue(); try ( - MariaDBContainer mariadbCustomConfig = new MariaDBContainer<>( + MariaDBContainer mariadbCustomConfig = new MariaDBContainer( MariaDBTestImages.MARIADB_IMAGE.withTag("10.3.39") ) .withConfigurationOverride("somepath/mariadb_conf_override") @@ -132,7 +133,19 @@ public void testWithOnlyUserReadableCustomIniFile() throws Exception { } } - private void assertThatCustomIniFileWasUsed(MariaDBContainer mariadb) throws SQLException { + @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) diff --git a/modules/mariadb/src/test/java/org/testcontainers/mariadb/MariaDBR2DBCDatabaseContainerTest.java b/modules/mariadb/src/test/java/org/testcontainers/mariadb/MariaDBR2DBCDatabaseContainerTest.java new file mode 100644 index 00000000000..8260a6937df --- /dev/null +++ b/modules/mariadb/src/test/java/org/testcontainers/mariadb/MariaDBR2DBCDatabaseContainerTest.java @@ -0,0 +1,23 @@ +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")); + } +} diff --git a/modules/milvus/build.gradle b/modules/milvus/build.gradle new file mode 100644 index 00000000000..613971697b6 --- /dev/null +++ b/modules/milvus/build.gradle @@ -0,0 +1,7 @@ +description = "Testcontainers :: Milvus" + +dependencies { + api project(':testcontainers') + + testImplementation 'io.milvus:milvus-sdk-java:2.6.17' +} diff --git a/modules/milvus/src/main/java/org/testcontainers/milvus/MilvusContainer.java b/modules/milvus/src/main/java/org/testcontainers/milvus/MilvusContainer.java new file mode 100644 index 00000000000..57b1b483cf8 --- /dev/null +++ b/modules/milvus/src/main/java/org/testcontainers/milvus/MilvusContainer.java @@ -0,0 +1,61 @@ +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); + } +} diff --git a/modules/milvus/src/main/resources/testcontainers/embedEtcd.yaml b/modules/milvus/src/main/resources/testcontainers/embedEtcd.yaml new file mode 100644 index 00000000000..2bd73bbd59f --- /dev/null +++ b/modules/milvus/src/main/resources/testcontainers/embedEtcd.yaml @@ -0,0 +1,2 @@ +listen-client-urls: http://0.0.0.0:2379 +advertise-client-urls: http://0.0.0.0:2379 diff --git a/modules/milvus/src/test/java/org/testcontainers/milvus/MilvusContainerTest.java b/modules/milvus/src/test/java/org/testcontainers/milvus/MilvusContainerTest.java new file mode 100644 index 00000000000..71ce05655f2 --- /dev/null +++ b/modules/milvus/src/test/java/org/testcontainers/milvus/MilvusContainerTest.java @@ -0,0 +1,66 @@ +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"); + } +} diff --git a/modules/milvus/src/test/resources/logback-test.xml b/modules/milvus/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..83ef7a1a3ef --- /dev/null +++ b/modules/milvus/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + diff --git a/modules/minio/build.gradle b/modules/minio/build.gradle index d53768d81e2..ce592fced2d 100644 --- a/modules/minio/build.gradle +++ b/modules/minio/build.gradle @@ -3,6 +3,5 @@ description = "Testcontainers :: MinIO" dependencies { api project(':testcontainers') - testImplementation("io.minio:minio:8.5.7") - testImplementation 'org.assertj:assertj-core:3.25.1' + testImplementation("io.minio:minio:9.0.3") } diff --git a/modules/minio/src/test/java/org/testcontainers/containers/MinIOContainerTest.java b/modules/minio/src/test/java/org/testcontainers/containers/MinIOContainerTest.java index 563a77852c5..0998243de8e 100644 --- a/modules/minio/src/test/java/org/testcontainers/containers/MinIOContainerTest.java +++ b/modules/minio/src/test/java/org/testcontainers/containers/MinIOContainerTest.java @@ -6,16 +6,16 @@ import io.minio.StatObjectArgs; import io.minio.StatObjectResponse; import io.minio.UploadObjectArgs; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.net.URL; import static org.assertj.core.api.Assertions.assertThat; -public class MinIOContainerTest { +class MinIOContainerTest { @Test - public void testBasicUsage() throws Exception { + void testBasicUsage() throws Exception { try ( // minioContainer { MinIOContainer container = new MinIOContainer("minio/minio:RELEASE.2023-09-04T19-57-37Z"); @@ -57,7 +57,7 @@ public void testBasicUsage() throws Exception { } @Test - public void testDefaultUserPassword() { + void testDefaultUserPassword() { try (MinIOContainer container = new MinIOContainer("minio/minio:RELEASE.2023-09-04T19-57-37Z")) { container.start(); assertThat(container.getUserName()).isEqualTo("minioadmin"); @@ -66,7 +66,7 @@ public void testDefaultUserPassword() { } @Test - public void testOverwriteUserPassword() { + void testOverwriteUserPassword() { try ( // minioOverrides { MinIOContainer container = new MinIOContainer("minio/minio:RELEASE.2023-09-04T19-57-37Z") diff --git a/modules/mockserver/build.gradle b/modules/mockserver/build.gradle index b12cae7ad4b..2ff2e9f34c6 100644 --- a/modules/mockserver/build.gradle +++ b/modules/mockserver/build.gradle @@ -4,5 +4,5 @@ dependencies { api project(':testcontainers') testImplementation 'org.mock-server:mockserver-client-java:5.15.0' - testImplementation 'org.assertj:assertj-core:3.25.1' + testImplementation 'io.rest-assured:rest-assured:5.5.7' } diff --git a/modules/mockserver/src/main/java/org/testcontainers/containers/MockServerContainer.java b/modules/mockserver/src/main/java/org/testcontainers/containers/MockServerContainer.java index 9ca936d1656..47d2399d934 100644 --- a/modules/mockserver/src/main/java/org/testcontainers/containers/MockServerContainer.java +++ b/modules/mockserver/src/main/java/org/testcontainers/containers/MockServerContainer.java @@ -4,7 +4,11 @@ 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"); diff --git a/modules/mockserver/src/main/java/org/testcontainers/mockserver/MockServerContainer.java b/modules/mockserver/src/main/java/org/testcontainers/mockserver/MockServerContainer.java new file mode 100644 index 00000000000..cbfcbc9fd66 --- /dev/null +++ b/modules/mockserver/src/main/java/org/testcontainers/mockserver/MockServerContainer.java @@ -0,0 +1,49 @@ +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); + } +} diff --git a/modules/mockserver/src/test/java/org/testcontainers/containers/MockServerContainerRuleTest.java b/modules/mockserver/src/test/java/org/testcontainers/containers/MockServerContainerRuleTest.java deleted file mode 100644 index eaf240f0fe2..00000000000 --- a/modules/mockserver/src/test/java/org/testcontainers/containers/MockServerContainerRuleTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.testcontainers.containers; - -import org.junit.Rule; -import org.junit.Test; -import org.mockserver.client.MockServerClient; -import org.testcontainers.utility.DockerImageName; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockserver.model.HttpRequest.request; -import static org.mockserver.model.HttpResponse.response; - -public class MockServerContainerRuleTest { - - // creatingProxy { - public static final DockerImageName MOCKSERVER_IMAGE = DockerImageName - .parse("mockserver/mockserver") - .withTag("mockserver-" + MockServerClient.class.getPackage().getImplementationVersion()); - - @Rule - public MockServerContainer mockServer = new MockServerContainer(MOCKSERVER_IMAGE); - - // } - - @Test - public void shouldReturnExpectation() throws Exception { - // testSimpleExpectation { - try ( - MockServerClient mockServerClient = new MockServerClient(mockServer.getHost(), mockServer.getServerPort()) - ) { - mockServerClient - .when(request().withPath("/person").withQueryStringParameter("name", "peter")) - .respond(response().withBody("Peter the person!")); - - // ...a GET request to '/person?name=peter' returns "Peter the person!" - - assertThat(SimpleHttpClient.responseFromMockserver(mockServer, "/person?name=peter")) - .as("Expectation returns expected response body") - .contains("Peter the person"); - } - // } - } -} diff --git a/modules/mockserver/src/test/java/org/testcontainers/containers/SimpleHttpClient.java b/modules/mockserver/src/test/java/org/testcontainers/containers/SimpleHttpClient.java deleted file mode 100644 index 6c7e75c4622..00000000000 --- a/modules/mockserver/src/test/java/org/testcontainers/containers/SimpleHttpClient.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.testcontainers.containers; - -import lombok.Cleanup; -import org.mockserver.configuration.Configuration; -import org.mockserver.logging.MockServerLogger; -import org.mockserver.socket.tls.KeyStoreFactory; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.URL; -import java.net.URLConnection; - -import javax.net.ssl.HttpsURLConnection; - -public class SimpleHttpClient { - - public static String responseFromMockserver(MockServerContainer mockServer, String path) throws IOException { - URLConnection urlConnection = new URL(mockServer.getEndpoint() + path).openConnection(); - @Cleanup - BufferedReader reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); - return reader.readLine(); - } - - public static String secureResponseFromMockserver(MockServerContainer mockServer, String path) throws IOException { - HttpsURLConnection httpUrlConnection = (HttpsURLConnection) new URL(mockServer.getSecureEndpoint() + path) - .openConnection(); - try { - httpUrlConnection.setSSLSocketFactory( - new KeyStoreFactory(Configuration.configuration(), new MockServerLogger()) - .sslContext() - .getSocketFactory() - ); - @Cleanup - BufferedReader reader = new BufferedReader(new InputStreamReader(httpUrlConnection.getInputStream())); - return reader.readLine(); - } finally { - httpUrlConnection.disconnect(); - } - } -} diff --git a/modules/mockserver/src/test/java/org/testcontainers/containers/MockServerContainerTest.java b/modules/mockserver/src/test/java/org/testcontainers/mockserver/MockServerContainerTest.java similarity index 58% rename from modules/mockserver/src/test/java/org/testcontainers/containers/MockServerContainerTest.java rename to modules/mockserver/src/test/java/org/testcontainers/mockserver/MockServerContainerTest.java index 3f4a8fef293..53325234b8b 100644 --- a/modules/mockserver/src/test/java/org/testcontainers/containers/MockServerContainerTest.java +++ b/modules/mockserver/src/test/java/org/testcontainers/mockserver/MockServerContainerTest.java @@ -1,22 +1,32 @@ -package org.testcontainers.containers; +package org.testcontainers.mockserver; -import org.junit.Test; +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; -public class MockServerContainerTest { +class MockServerContainerTest { public static final DockerImageName MOCKSERVER_IMAGE = DockerImageName .parse("mockserver/mockserver") .withTag("mockserver-" + MockServerClient.class.getPackage().getImplementationVersion()); @Test - public void shouldCallActualMockserverVersion() throws Exception { - try (MockServerContainer mockServer = new MockServerContainer(MOCKSERVER_IMAGE)) { + void shouldCallActualMockserverVersion() { + try ( // creatingProxy { + MockServerContainer mockServer = new MockServerContainer(MOCKSERVER_IMAGE) + // } + ) { mockServer.start(); String expectedBody = "Hello World!"; @@ -26,7 +36,7 @@ public void shouldCallActualMockserverVersion() throws Exception { client.when(request().withPath("/hello")).respond(response().withBody(expectedBody)); - assertThat(SimpleHttpClient.responseFromMockserver(mockServer, "/hello")) + assertThat(given().when().get(mockServer.getEndpoint() + "/hello").then().extract().body().asString()) .as("MockServer returns correct result") .isEqualTo(expectedBody); } @@ -34,7 +44,7 @@ public void shouldCallActualMockserverVersion() throws Exception { } @Test - public void shouldCallMockserverUsingTlsProtocol() throws Exception { + void shouldCallMockserverUsingTlsProtocol() { try (MockServerContainer mockServer = new MockServerContainer(MOCKSERVER_IMAGE)) { mockServer.start(); @@ -48,7 +58,7 @@ public void shouldCallMockserverUsingTlsProtocol() throws Exception { client.when(request().withPath("/hello")).respond(response().withBody(expectedBody)); - assertThat(SimpleHttpClient.secureResponseFromMockserver(mockServer, "/hello")) + assertThat(secureResponseFromMockserver(mockServer)) .as("MockServer returns correct result") .isEqualTo(expectedBody); } @@ -56,7 +66,7 @@ public void shouldCallMockserverUsingTlsProtocol() throws Exception { } @Test - public void shouldCallMockserverUsingMutualTlsProtocol() throws Exception { + void shouldCallMockserverUsingMutualTlsProtocol() { try ( MockServerContainer mockServer = new MockServerContainer(MOCKSERVER_IMAGE) .withEnv("MOCKSERVER_TLS_MUTUAL_AUTHENTICATION_REQUIRED", "true") @@ -73,7 +83,7 @@ public void shouldCallMockserverUsingMutualTlsProtocol() throws Exception { client.when(request().withPath("/hello")).respond(response().withBody(expectedBody)); - assertThat(SimpleHttpClient.secureResponseFromMockserver(mockServer, "/hello")) + assertThat(secureResponseFromMockserver(mockServer)) .as("MockServer returns correct result") .isEqualTo(expectedBody); } @@ -81,9 +91,31 @@ public void shouldCallMockserverUsingMutualTlsProtocol() throws Exception { } @Test - public void newVersionStartsWithDefaultWaitStrategy() { + 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(); + } } diff --git a/modules/mongodb/build.gradle b/modules/mongodb/build.gradle index c1b1f21b235..8e9f1420ddf 100644 --- a/modules/mongodb/build.gradle +++ b/modules/mongodb/build.gradle @@ -3,12 +3,5 @@ description = "Testcontainers :: MongoDB" dependencies { api project(':testcontainers') - testImplementation("org.mongodb:mongodb-driver-sync:4.11.1") - testImplementation 'org.assertj:assertj-core:3.25.1' -} - -tasks.japicmp { - methodExcludes = [ - "org.testcontainers.containers.MongoDBContainer#configure()" - ] + testImplementation("org.mongodb:mongodb-driver-sync:5.1.4") } diff --git a/modules/mongodb/src/main/java/org/testcontainers/containers/MongoDBContainer.java b/modules/mongodb/src/main/java/org/testcontainers/containers/MongoDBContainer.java index 1e6749f8aed..d30f45739c0 100644 --- a/modules/mongodb/src/main/java/org/testcontainers/containers/MongoDBContainer.java +++ b/modules/mongodb/src/main/java/org/testcontainers/containers/MongoDBContainer.java @@ -13,15 +13,26 @@ /** * Testcontainers implementation for MongoDB. *

    - * Supported image: {@code mongo} + * 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; @@ -48,7 +59,7 @@ public MongoDBContainer(@NonNull final String dockerImageName) { public MongoDBContainer(final DockerImageName dockerImageName) { super(dockerImageName); - dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, COMMUNITY_SERVER_IMAGE, ENTERPRISE_SERVER_IMAGE); } @Override @@ -87,7 +98,7 @@ protected void containerIsStarted(InspectContainerResponse containerInfo, boolea } /** - * Gets a connection string url, unlike {@link #getReplicaSetUrl} this does point to a + * Gets a connection string url, unlike {@link #getReplicaSetUrl} this does not point to a * database * @return a connection url pointing to a mongodb instance */ diff --git a/modules/mongodb/src/main/java/org/testcontainers/mongodb/MongoDBAtlasLocalContainer.java b/modules/mongodb/src/main/java/org/testcontainers/mongodb/MongoDBAtlasLocalContainer.java new file mode 100644 index 00000000000..79e44d3ffb0 --- /dev/null +++ b/modules/mongodb/src/main/java/org/testcontainers/mongodb/MongoDBAtlasLocalContainer.java @@ -0,0 +1,68 @@ +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; + } +} diff --git a/modules/mongodb/src/main/java/org/testcontainers/mongodb/MongoDBContainer.java b/modules/mongodb/src/main/java/org/testcontainers/mongodb/MongoDBContainer.java new file mode 100644 index 00000000000..c61be6675ba --- /dev/null +++ b/modules/mongodb/src/main/java/org/testcontainers/mongodb/MongoDBContainer.java @@ -0,0 +1,207 @@ +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; + } +} diff --git a/modules/mongodb/src/test/java/org/testcontainers/containers/MongoDBContainerTest.java b/modules/mongodb/src/test/java/org/testcontainers/mongodb/AbstractMongo.java similarity index 52% rename from modules/mongodb/src/test/java/org/testcontainers/containers/MongoDBContainerTest.java rename to modules/mongodb/src/test/java/org/testcontainers/mongodb/AbstractMongo.java index f5cb4dd5d90..8bd0f99dbd5 100644 --- a/modules/mongodb/src/test/java/org/testcontainers/containers/MongoDBContainerTest.java +++ b/modules/mongodb/src/test/java/org/testcontainers/mongodb/AbstractMongo.java @@ -1,4 +1,4 @@ -package org.testcontainers.containers; +package org.testcontainers.mongodb; import com.mongodb.ReadConcern; import com.mongodb.ReadPreference; @@ -10,31 +10,12 @@ import com.mongodb.client.MongoCollection; import com.mongodb.client.TransactionBody; import org.bson.Document; -import org.junit.Test; -import org.testcontainers.utility.DockerImageName; import static org.assertj.core.api.Assertions.assertThat; -public class MongoDBContainerTest { +public class AbstractMongo { - /** - * Taken from https://docs.mongodb.com - */ - @Test - public void shouldExecuteTransactions() { - try ( - // creatingMongoDBContainer { - final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:4.0.10")) - // } - ) { - // startingMongoDBContainer { - mongoDBContainer.start(); - // } - executeTx(mongoDBContainer); - } - } - - private void executeTx(MongoDBContainer mongoDBContainer) { + protected void executeTx(MongoDBContainer mongoDBContainer) { final MongoClient mongoSyncClientBase = MongoClients.create(mongoDBContainer.getConnectionString()); final MongoClient mongoSyncClient = MongoClients.create(mongoDBContainer.getReplicaSetUrl()); mongoSyncClient @@ -82,43 +63,4 @@ private void executeTx(MongoDBContainer mongoDBContainer) { mongoSyncClient.close(); } } - - @Test - public void supportsMongoDB_4_4() { - try (final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:4.4"))) { - mongoDBContainer.start(); - } - } - - @Test - public void shouldTestDatabaseName() { - try (final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:4.0.10"))) { - mongoDBContainer.start(); - final String databaseName = "my-db"; - assertThat(mongoDBContainer.getReplicaSetUrl(databaseName)).endsWith(databaseName); - } - } - - @Test - public void shouldSupportSharding() { - try (final MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:6").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)); - } } diff --git a/modules/mongodb/src/test/java/org/testcontainers/mongodb/AtlasLocalDataAccess.java b/modules/mongodb/src/test/java/org/testcontainers/mongodb/AtlasLocalDataAccess.java new file mode 100644 index 00000000000..c3e6aff41f9 --- /dev/null +++ b/modules/mongodb/src/test/java/org/testcontainers/mongodb/AtlasLocalDataAccess.java @@ -0,0 +1,177 @@ +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 + '}'; + } + } +} diff --git a/modules/mongodb/src/test/java/org/testcontainers/mongodb/CompatibleImageTest.java b/modules/mongodb/src/test/java/org/testcontainers/mongodb/CompatibleImageTest.java new file mode 100644 index 00000000000..b877dca8954 --- /dev/null +++ b/modules/mongodb/src/test/java/org/testcontainers/mongodb/CompatibleImageTest.java @@ -0,0 +1,59 @@ +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)); + } +} diff --git a/modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBAtlasLocalContainerTest.java b/modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBAtlasLocalContainerTest.java new file mode 100644 index 00000000000..7bc7a39808b --- /dev/null +++ b/modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBAtlasLocalContainerTest.java @@ -0,0 +1,99 @@ +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) + ); + } + } + } +} diff --git a/modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBContainerTest.java b/modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBContainerTest.java new file mode 100644 index 00000000000..816243d769e --- /dev/null +++ b/modules/mongodb/src/test/java/org/testcontainers/mongodb/MongoDBContainerTest.java @@ -0,0 +1,41 @@ +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); + } + } +} diff --git a/modules/mongodb/src/test/resources/atlas-local-index.json b/modules/mongodb/src/test/resources/atlas-local-index.json new file mode 100644 index 00000000000..1bca016025f --- /dev/null +++ b/modules/mongodb/src/test/resources/atlas-local-index.json @@ -0,0 +1,18 @@ +{ + "mappings": { + "dynamic": false, + "fields": { + "test": { + "type": "string" + }, + "test2": { + "type": "number", + "representation": "int64", + "indexDoubles": false + }, + "test3": { + "type": "boolean" + } + } + } +} diff --git a/modules/mssqlserver/build.gradle b/modules/mssqlserver/build.gradle index 6f67d160245..cb3e7a5b55e 100644 --- a/modules/mssqlserver/build.gradle +++ b/modules/mssqlserver/build.gradle @@ -1,20 +1,17 @@ description = "Testcontainers :: MS SQL Server" dependencies { - annotationProcessor 'com.google.auto.service:auto-service:1.1.1' - compileOnly 'com.google.auto.service:auto-service:1.1.1' + api project(':testcontainers-jdbc') - api project(':jdbc') + compileOnly project(':testcontainers-r2dbc') + compileOnly 'io.r2dbc:r2dbc-mssql:1.0.5.RELEASE' - compileOnly project(':r2dbc') - compileOnly 'io.r2dbc:r2dbc-mssql:1.0.2.RELEASE' + testImplementation project(':testcontainers-jdbc-test') + testImplementation 'com.microsoft.sqlserver:mssql-jdbc:13.4.0.jre11' - testImplementation project(':jdbc-test') - testImplementation 'com.microsoft.sqlserver:mssql-jdbc:12.5.0.jre8-preview' - - testImplementation project(':r2dbc') - testRuntimeOnly 'io.r2dbc:r2dbc-mssql:1.0.2.RELEASE' + testImplementation project(':testcontainers-r2dbc') + testRuntimeOnly 'io.r2dbc:r2dbc-mssql:1.0.5.RELEASE' // MSSQL's wait strategy requires the JDBC driver - testImplementation testFixtures(project(':r2dbc')) + testImplementation testFixtures(project(':testcontainers-r2dbc')) } diff --git a/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerProvider.java b/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerProvider.java index fe7ada669bd..f829ddb8f1f 100644 --- a/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerProvider.java +++ b/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerProvider.java @@ -1,6 +1,5 @@ package org.testcontainers.containers; -import com.google.auto.service.AutoService; import io.r2dbc.mssql.MssqlConnectionFactoryProvider; import io.r2dbc.spi.ConnectionFactoryMetadata; import io.r2dbc.spi.ConnectionFactoryOptions; @@ -9,7 +8,6 @@ import javax.annotation.Nullable; -@AutoService(R2DBCDatabaseContainerProvider.class) public class MSSQLR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider { static final String DRIVER = MssqlConnectionFactoryProvider.MSSQL_DRIVER; diff --git a/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainer.java b/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainer.java index 51ee7989f33..07f8a064c00 100644 --- a/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainer.java +++ b/modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainer.java @@ -13,7 +13,10 @@ * 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"); @@ -72,13 +75,13 @@ public Set getLivenessCheckPortNumbers() { @Override protected void configure() { - // If license was not accepted programatically, check if it was accepted via resource file + // 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("SA_PASSWORD", password); + addEnv("MSSQL_SA_PASSWORD", password); } /** diff --git a/modules/mssqlserver/src/main/java/org/testcontainers/mssqlserver/MSSQLR2DBCDatabaseContainer.java b/modules/mssqlserver/src/main/java/org/testcontainers/mssqlserver/MSSQLR2DBCDatabaseContainer.java new file mode 100644 index 00000000000..d8d7740e01d --- /dev/null +++ b/modules/mssqlserver/src/main/java/org/testcontainers/mssqlserver/MSSQLR2DBCDatabaseContainer.java @@ -0,0 +1,59 @@ +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(); + } +} diff --git a/modules/mssqlserver/src/main/java/org/testcontainers/mssqlserver/MSSQLServerContainer.java b/modules/mssqlserver/src/main/java/org/testcontainers/mssqlserver/MSSQLServerContainer.java new file mode 100644 index 00000000000..6ad74ea7e72 --- /dev/null +++ b/modules/mssqlserver/src/main/java/org/testcontainers/mssqlserver/MSSQLServerContainer.java @@ -0,0 +1,156 @@ +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 (%)." + ); + } + } +} diff --git a/modules/mssqlserver/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider b/modules/mssqlserver/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider new file mode 100644 index 00000000000..0ec6b22ddf6 --- /dev/null +++ b/modules/mssqlserver/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider @@ -0,0 +1 @@ +org.testcontainers.containers.MSSQLR2DBCDatabaseContainerProvider diff --git a/modules/mssqlserver/src/test/java/org/testcontainers/MSSQLServerTestImages.java b/modules/mssqlserver/src/test/java/org/testcontainers/MSSQLServerTestImages.java index 0cec042e19f..804691e2baf 100644 --- a/modules/mssqlserver/src/test/java/org/testcontainers/MSSQLServerTestImages.java +++ b/modules/mssqlserver/src/test/java/org/testcontainers/MSSQLServerTestImages.java @@ -3,5 +3,5 @@ import org.testcontainers.utility.DockerImageName; public interface MSSQLServerTestImages { - DockerImageName MSSQL_SERVER_IMAGE = DockerImageName.parse("mcr.microsoft.com/mssql/server:2017-CU12"); + DockerImageName MSSQL_SERVER_IMAGE = DockerImageName.parse("mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04"); } diff --git a/modules/mssqlserver/src/test/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerTest.java b/modules/mssqlserver/src/test/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerTest.java index 8c0c9ab1c2b..a0d3106f9c6 100644 --- a/modules/mssqlserver/src/test/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerTest.java +++ b/modules/mssqlserver/src/test/java/org/testcontainers/containers/MSSQLR2DBCDatabaseContainerTest.java @@ -13,7 +13,7 @@ protected ConnectionFactoryOptions getOptions(MSSQLServerContainer container) @Override protected String createR2DBCUrl() { - return "r2dbc:tc:sqlserver:///?TC_IMAGE_TAG=2017-CU12"; + return "r2dbc:tc:sqlserver:///?TC_IMAGE_TAG=2022-CU14-ubuntu-22.04"; } @Override diff --git a/modules/mssqlserver/src/test/java/org/testcontainers/jdbc/mssqlserver/MSSQLServerJDBCDriverTest.java b/modules/mssqlserver/src/test/java/org/testcontainers/jdbc/mssqlserver/MSSQLServerJDBCDriverTest.java index bdb623136fb..9f616ab10f8 100644 --- a/modules/mssqlserver/src/test/java/org/testcontainers/jdbc/mssqlserver/MSSQLServerJDBCDriverTest.java +++ b/modules/mssqlserver/src/test/java/org/testcontainers/jdbc/mssqlserver/MSSQLServerJDBCDriverTest.java @@ -1,21 +1,17 @@ package org.testcontainers.jdbc.mssqlserver; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; -@RunWith(Parameterized.class) -public class MSSQLServerJDBCDriverTest extends AbstractJDBCDriverTest { +class MSSQLServerJDBCDriverTest extends AbstractJDBCDriverTest { - @Parameterized.Parameters(name = "{index} - {0}") public static Iterable data() { return Arrays.asList( new Object[][] { { - "jdbc:tc:sqlserver:2017-CU12://hostname:hostport;databaseName=databasename", + "jdbc:tc:sqlserver:2022-CU14-ubuntu-22.04://hostname:hostport;databaseName=databasename", EnumSet.noneOf(Options.class), }, } diff --git a/modules/mssqlserver/src/test/java/org/testcontainers/junit/mssqlserver/CustomPasswordMSSQLServerTest.java b/modules/mssqlserver/src/test/java/org/testcontainers/junit/mssqlserver/CustomPasswordMSSQLServerTest.java deleted file mode 100644 index 27075d6a3fe..00000000000 --- a/modules/mssqlserver/src/test/java/org/testcontainers/junit/mssqlserver/CustomPasswordMSSQLServerTest.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.testcontainers.junit.mssqlserver; - -import org.apache.commons.lang3.RandomStringUtils; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.testcontainers.MSSQLServerTestImages; -import org.testcontainers.containers.MSSQLServerContainer; - -import java.util.Arrays; -import java.util.Collection; - -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 - */ -@RunWith(Parameterized.class) -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 = "_(!)_"; - - private String password; - - private Boolean valid; - - public CustomPasswordMSSQLServerTest(String password, Boolean valid) { - this.password = password; - this.valid = valid; - } - - @Parameterized.Parameters - public static Collection data() { - return Arrays.asList( - new Object[][] { - new Object[] { null, false }, - // too short - { "abc123", false }, - // too long - { RandomStringUtils.randomAlphabetic(129), false }, - // only 2 categories - { UPPER_CASE_LETTERS + NUMBERS, false }, - { UPPER_CASE_LETTERS + SPECIAL_CHARS, false }, - { LOWER_CASE_LETTERS + NUMBERS, false }, - { LOWER_CASE_LETTERS + SPECIAL_CHARS, false }, - { NUMBERS + SPECIAL_CHARS, false }, - // 3 categories - { UPPER_CASE_LETTERS + LOWER_CASE_LETTERS + NUMBERS, true }, - { UPPER_CASE_LETTERS + LOWER_CASE_LETTERS + SPECIAL_CHARS, true }, - { UPPER_CASE_LETTERS + NUMBERS + SPECIAL_CHARS, true }, - { LOWER_CASE_LETTERS + NUMBERS + SPECIAL_CHARS, true }, - // 4 categories - { UPPER_CASE_LETTERS + LOWER_CASE_LETTERS + NUMBERS + SPECIAL_CHARS, true }, - } - ); - } - - @Test - public void runPasswordTests() { - try { - new MSSQLServerContainer<>(MSSQLServerTestImages.MSSQL_SERVER_IMAGE).withPassword(this.password); - if (!valid) { - fail("Password " + this.password + " is not valid. Expected exception"); - } - } catch (IllegalArgumentException e) { - if (valid) { - fail("Password " + this.password + " should have been validated"); - } - } - } -} diff --git a/modules/mssqlserver/src/test/java/org/testcontainers/junit/mssqlserver/CustomizableMSSQLServerTest.java b/modules/mssqlserver/src/test/java/org/testcontainers/junit/mssqlserver/CustomizableMSSQLServerTest.java deleted file mode 100644 index 4cfc34f9e73..00000000000 --- a/modules/mssqlserver/src/test/java/org/testcontainers/junit/mssqlserver/CustomizableMSSQLServerTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.testcontainers.junit.mssqlserver; - -import org.junit.Test; -import org.testcontainers.containers.MSSQLServerContainer; -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; - -public class CustomizableMSSQLServerTest extends AbstractContainerDatabaseTest { - - private static final String STRONG_PASSWORD = "myStrong(!)Password"; - - @Test - public void testSqlServerConnection() throws SQLException { - try ( - MSSQLServerContainer mssqlServerContainer = new MSSQLServerContainer<>( - DockerImageName.parse("mcr.microsoft.com/mssql/server:2017-CU12") - ) - .withPassword(STRONG_PASSWORD) - ) { - mssqlServerContainer.start(); - - ResultSet resultSet = performQuery(mssqlServerContainer, mssqlServerContainer.getTestQueryString()); - int resultSetInt = resultSet.getInt(1); - assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); - } - } -} diff --git a/modules/mssqlserver/src/test/java/org/testcontainers/mssqlserver/CustomPasswordMSSQLServerTest.java b/modules/mssqlserver/src/test/java/org/testcontainers/mssqlserver/CustomPasswordMSSQLServerTest.java new file mode 100644 index 00000000000..faaaabd7d01 --- /dev/null +++ b/modules/mssqlserver/src/test/java/org/testcontainers/mssqlserver/CustomPasswordMSSQLServerTest.java @@ -0,0 +1,65 @@ +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"); + } + } + } +} diff --git a/modules/mssqlserver/src/test/java/org/testcontainers/mssqlserver/MSSQLR2DBCDatabaseContainerTest.java b/modules/mssqlserver/src/test/java/org/testcontainers/mssqlserver/MSSQLR2DBCDatabaseContainerTest.java new file mode 100644 index 00000000000..7b51e8f1f93 --- /dev/null +++ b/modules/mssqlserver/src/test/java/org/testcontainers/mssqlserver/MSSQLR2DBCDatabaseContainerTest.java @@ -0,0 +1,23 @@ +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); + } +} diff --git a/modules/mssqlserver/src/test/java/org/testcontainers/junit/mssqlserver/SimpleMSSQLServerTest.java b/modules/mssqlserver/src/test/java/org/testcontainers/mssqlserver/MSSQLServerContainerTest.java similarity index 60% rename from modules/mssqlserver/src/test/java/org/testcontainers/junit/mssqlserver/SimpleMSSQLServerTest.java rename to modules/mssqlserver/src/test/java/org/testcontainers/mssqlserver/MSSQLServerContainerTest.java index 2827544cade..52269f11b9e 100644 --- a/modules/mssqlserver/src/test/java/org/testcontainers/junit/mssqlserver/SimpleMSSQLServerTest.java +++ b/modules/mssqlserver/src/test/java/org/testcontainers/mssqlserver/MSSQLServerContainerTest.java @@ -1,9 +1,9 @@ -package org.testcontainers.junit.mssqlserver; +package org.testcontainers.mssqlserver; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.MSSQLServerTestImages; -import org.testcontainers.containers.MSSQLServerContainer; import org.testcontainers.db.AbstractContainerDatabaseTest; +import org.testcontainers.utility.DockerImageName; import java.sql.ResultSet; import java.sql.SQLException; @@ -13,12 +13,16 @@ import static org.assertj.core.api.Assertions.assertThat; -public class SimpleMSSQLServerTest extends AbstractContainerDatabaseTest { +class MSSQLServerContainerTest extends AbstractContainerDatabaseTest { @Test - public void testSimple() throws SQLException { - try ( - MSSQLServerContainer mssqlServer = new MSSQLServerContainer<>(MSSQLServerTestImages.MSSQL_SERVER_IMAGE) + 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"); @@ -30,9 +34,9 @@ public void testSimple() throws SQLException { } @Test - public void testWithAdditionalUrlParamInJdbcUrl() { + void testWithAdditionalUrlParamInJdbcUrl() { try ( - MSSQLServerContainer mssqlServer = new MSSQLServerContainer<>(MSSQLServerTestImages.MSSQL_SERVER_IMAGE) + MSSQLServerContainer mssqlServer = new MSSQLServerContainer(MSSQLServerTestImages.MSSQL_SERVER_IMAGE) .withUrlParam("integratedSecurity", "false") .withUrlParam("applicationName", "MyApp") ) { @@ -44,10 +48,8 @@ public void testWithAdditionalUrlParamInJdbcUrl() { } @Test - public void testSetupDatabase() throws SQLException { - try ( - MSSQLServerContainer mssqlServer = new MSSQLServerContainer<>(MSSQLServerTestImages.MSSQL_SERVER_IMAGE) - ) { + void testSetupDatabase() throws SQLException { + try (MSSQLServerContainer mssqlServer = new MSSQLServerContainer(MSSQLServerTestImages.MSSQL_SERVER_IMAGE)) { mssqlServer.start(); DataSource ds = getDataSource(mssqlServer); Statement statement = ds.getConnection().createStatement(); @@ -66,7 +68,23 @@ public void testSetupDatabase() throws SQLException { } } - private void assertHasCorrectExposedAndLivenessCheckPorts(MSSQLServerContainer mssqlServer) { + @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)); diff --git a/modules/mssqlserver/src/test/resources/container-license-acceptance.txt b/modules/mssqlserver/src/test/resources/container-license-acceptance.txt index 8a704b9c066..5ae6ecbd4d1 100644 --- a/modules/mssqlserver/src/test/resources/container-license-acceptance.txt +++ b/modules/mssqlserver/src/test/resources/container-license-acceptance.txt @@ -1 +1 @@ -mcr.microsoft.com/mssql/server:2017-CU12 +mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04 diff --git a/modules/mysql/build.gradle b/modules/mysql/build.gradle index a61819b1542..2cf8b17d64f 100644 --- a/modules/mysql/build.gradle +++ b/modules/mysql/build.gradle @@ -1,19 +1,16 @@ description = "Testcontainers :: JDBC :: MySQL" dependencies { - annotationProcessor 'com.google.auto.service:auto-service:1.1.1' - compileOnly 'com.google.auto.service:auto-service:1.1.1' + api project(':testcontainers-jdbc') - api project(':jdbc') + compileOnly project(':testcontainers-r2dbc') + compileOnly 'io.asyncer:r2dbc-mysql:1.4.1' - compileOnly project(':r2dbc') - compileOnly 'io.asyncer:r2dbc-mysql:1.0.6' + testImplementation project(':testcontainers-jdbc-test') + testRuntimeOnly 'com.mysql:mysql-connector-j:9.6.0' - testImplementation project(':jdbc-test') - testRuntimeOnly 'mysql:mysql-connector-java:8.0.33' + testImplementation testFixtures(project(':testcontainers-r2dbc')) + testRuntimeOnly 'io.asyncer:r2dbc-mysql:1.4.1' - testImplementation testFixtures(project(':r2dbc')) - testRuntimeOnly 'io.asyncer:r2dbc-mysql:1.0.6' - - compileOnly 'org.jetbrains:annotations:24.1.0' + compileOnly 'org.jetbrains:annotations:26.1.0' } diff --git a/modules/mysql/src/main/java/org/testcontainers/containers/MySQLContainer.java b/modules/mysql/src/main/java/org/testcontainers/containers/MySQLContainer.java index 76ff754b616..36315b2fe48 100644 --- a/modules/mysql/src/main/java/org/testcontainers/containers/MySQLContainer.java +++ b/modules/mysql/src/main/java/org/testcontainers/containers/MySQLContainer.java @@ -12,7 +12,10 @@ * 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"; diff --git a/modules/mysql/src/main/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerProvider.java b/modules/mysql/src/main/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerProvider.java index bf74e8ec27a..97f7f4a243d 100644 --- a/modules/mysql/src/main/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerProvider.java +++ b/modules/mysql/src/main/java/org/testcontainers/containers/MySQLR2DBCDatabaseContainerProvider.java @@ -1,6 +1,5 @@ package org.testcontainers.containers; -import com.google.auto.service.AutoService; import io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider; import io.r2dbc.spi.ConnectionFactoryMetadata; import io.r2dbc.spi.ConnectionFactoryOptions; @@ -9,7 +8,6 @@ import javax.annotation.Nullable; -@AutoService(R2DBCDatabaseContainerProvider.class) public class MySQLR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider { static final String DRIVER = MySqlConnectionFactoryProvider.MYSQL_DRIVER; diff --git a/modules/mysql/src/main/java/org/testcontainers/mysql/MySQLContainer.java b/modules/mysql/src/main/java/org/testcontainers/mysql/MySQLContainer.java new file mode 100644 index 00000000000..25f876c9911 --- /dev/null +++ b/modules/mysql/src/main/java/org/testcontainers/mysql/MySQLContainer.java @@ -0,0 +1,160 @@ +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(); + } +} diff --git a/modules/mysql/src/main/java/org/testcontainers/mysql/MySQLR2DBCDatabaseContainer.java b/modules/mysql/src/main/java/org/testcontainers/mysql/MySQLR2DBCDatabaseContainer.java new file mode 100644 index 00000000000..d2a272559d9 --- /dev/null +++ b/modules/mysql/src/main/java/org/testcontainers/mysql/MySQLR2DBCDatabaseContainer.java @@ -0,0 +1,58 @@ +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(); + } +} diff --git a/modules/mysql/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider b/modules/mysql/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider new file mode 100644 index 00000000000..88cab78dc2e --- /dev/null +++ b/modules/mysql/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider @@ -0,0 +1 @@ +org.testcontainers.containers.MySQLR2DBCDatabaseContainerProvider diff --git a/modules/mysql/src/main/resources/mysql-default-conf/my.cnf b/modules/mysql/src/main/resources/mysql-default-conf/my.cnf index 6fc2129357f..b22aa7d9e93 100644 --- a/modules/mysql/src/main/resources/mysql-default-conf/my.cnf +++ b/modules/mysql/src/main/resources/mysql-default-conf/my.cnf @@ -43,7 +43,6 @@ innodb_data_file_path = ibdata1:10M:autoextend 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 diff --git a/modules/mysql/src/test/java/org/testcontainers/MySQLTestImages.java b/modules/mysql/src/test/java/org/testcontainers/MySQLTestImages.java index 2254754f2c2..6b3fe80ce4a 100644 --- a/modules/mysql/src/test/java/org/testcontainers/MySQLTestImages.java +++ b/modules/mysql/src/test/java/org/testcontainers/MySQLTestImages.java @@ -9,4 +9,6 @@ public class MySQLTestImages { 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"); } diff --git a/modules/mysql/src/test/java/org/testcontainers/containers/MySQLRootAccountTest.java b/modules/mysql/src/test/java/org/testcontainers/containers/MySQLRootAccountTest.java index 51ba5257fa3..a8737c4c74f 100644 --- a/modules/mysql/src/test/java/org/testcontainers/containers/MySQLRootAccountTest.java +++ b/modules/mysql/src/test/java/org/testcontainers/containers/MySQLRootAccountTest.java @@ -1,9 +1,8 @@ package org.testcontainers.containers; import lombok.extern.slf4j.Slf4j; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; +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; @@ -13,33 +12,32 @@ import java.sql.SQLException; @Slf4j -@RunWith(Parameterized.class) -public class MySQLRootAccountTest { +class MySQLRootAccountTest { - @Parameterized.Parameters(name = "{0}") public static DockerImageName[] params() { return new DockerImageName[] { MySQLTestImages.MYSQL_57_IMAGE, MySQLTestImages.MYSQL_80_IMAGE, MySQLTestImages.MYSQL_INNOVATION_IMAGE, + MySQLTestImages.MYSQL_93_IMAGE, }; } - @Parameterized.Parameter - public DockerImageName image; - - @Test - public void testRootAccountUsageWithDefaultPassword() throws SQLException { + @ParameterizedTest + @MethodSource("params") + void testRootAccountUsageWithDefaultPassword(DockerImageName image) throws SQLException { testWithDB(new MySQLContainer<>(image).withUsername("root")); } - @Test - public void testRootAccountUsageWithEmptyPassword() throws SQLException { + @ParameterizedTest + @MethodSource("params") + void testRootAccountUsageWithEmptyPassword(DockerImageName image) throws SQLException { testWithDB(new MySQLContainer<>(image).withUsername("root").withPassword("")); } - @Test - public void testRootAccountUsageWithCustomPassword() throws SQLException { + @ParameterizedTest + @MethodSource("params") + void testRootAccountUsageWithCustomPassword(DockerImageName image) throws SQLException { testWithDB(new MySQLContainer<>(image).withUsername("root").withPassword("not-default")); } diff --git a/modules/mysql/src/test/java/org/testcontainers/jdbc/mysql/JDBCDriverWithPoolTest.java b/modules/mysql/src/test/java/org/testcontainers/jdbc/mysql/JDBCDriverWithPoolTest.java index be48f623183..0075b2ebf7f 100644 --- a/modules/mysql/src/test/java/org/testcontainers/jdbc/mysql/JDBCDriverWithPoolTest.java +++ b/modules/mysql/src/test/java/org/testcontainers/jdbc/mysql/JDBCDriverWithPoolTest.java @@ -5,19 +5,19 @@ import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.ResultSetHandler; import org.apache.tomcat.jdbc.pool.PoolProperties; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; +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.Arrays; 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; @@ -29,7 +29,8 @@ * 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. */ -@RunWith(Parameterized.class) +@ParameterizedClass +@MethodSource("dataSourceSuppliers") public class JDBCDriverWithPoolTest { public static final String URL = @@ -37,9 +38,8 @@ public class JDBCDriverWithPoolTest { private final DataSource dataSource; - @Parameterized.Parameters - public static Iterable> dataSourceSuppliers() { - return Arrays.asList( + public static Stream> dataSourceSuppliers() { + return Stream.of( JDBCDriverWithPoolTest::getTomcatDataSourceWithDriverClassName, JDBCDriverWithPoolTest::getTomcatDataSource, JDBCDriverWithPoolTest::getHikariDataSourceWithDriverClassName, @@ -56,7 +56,7 @@ public JDBCDriverWithPoolTest(Supplier dataSourceSupplier) { private ExecutorService executorService = Executors.newFixedThreadPool(5); @Test - public void testMySQLWithConnectionPoolUsingSameContainer() throws SQLException, InterruptedException { + 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(() -> { diff --git a/modules/mysql/src/test/java/org/testcontainers/jdbc/mysql/MySQLDatabaseContainerDriverTest.java b/modules/mysql/src/test/java/org/testcontainers/jdbc/mysql/MySQLDatabaseContainerDriverTest.java index 7f5cf5eae1d..da73f21ae2e 100644 --- a/modules/mysql/src/test/java/org/testcontainers/jdbc/mysql/MySQLDatabaseContainerDriverTest.java +++ b/modules/mysql/src/test/java/org/testcontainers/jdbc/mysql/MySQLDatabaseContainerDriverTest.java @@ -1,6 +1,6 @@ package org.testcontainers.jdbc.mysql; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.jdbc.ContainerDatabaseDriver; import java.sql.Connection; @@ -11,10 +11,10 @@ import static org.assertj.core.api.Assertions.assertThat; -public class MySQLDatabaseContainerDriverTest { +class MySQLDatabaseContainerDriverTest { @Test - public void shouldRespectBothUrlPropertiesAndParameterProperties() throws SQLException { + void shouldRespectBothUrlPropertiesAndParameterProperties() throws SQLException { ContainerDatabaseDriver driver = new ContainerDatabaseDriver(); String url = "jdbc:tc:mysql:8.0.36://hostname/databasename?padCharsWithSpace=true"; Properties properties = new Properties(); diff --git a/modules/mysql/src/test/java/org/testcontainers/jdbc/mysql/MySQLJDBCDriverTest.java b/modules/mysql/src/test/java/org/testcontainers/jdbc/mysql/MySQLJDBCDriverTest.java index 526c2ab522c..b069e5fd0c8 100644 --- a/modules/mysql/src/test/java/org/testcontainers/jdbc/mysql/MySQLJDBCDriverTest.java +++ b/modules/mysql/src/test/java/org/testcontainers/jdbc/mysql/MySQLJDBCDriverTest.java @@ -1,16 +1,12 @@ package org.testcontainers.jdbc.mysql; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; -@RunWith(Parameterized.class) -public class MySQLJDBCDriverTest extends AbstractJDBCDriverTest { +class MySQLJDBCDriverTest extends AbstractJDBCDriverTest { - @Parameterized.Parameters(name = "{index} - {0}") public static Iterable data() { return Arrays.asList( new Object[][] { diff --git a/modules/mysql/src/test/java/org/testcontainers/junit/mysql/CustomizableMysqlTest.java b/modules/mysql/src/test/java/org/testcontainers/junit/mysql/CustomizableMysqlTest.java deleted file mode 100644 index e756631d8e1..00000000000 --- a/modules/mysql/src/test/java/org/testcontainers/junit/mysql/CustomizableMysqlTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.testcontainers.junit.mysql; - -import org.junit.Test; -import org.testcontainers.MySQLTestImages; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.db.AbstractContainerDatabaseTest; - -import java.sql.ResultSet; -import java.sql.SQLException; - -import static org.assertj.core.api.Assertions.assertThat; - -public class CustomizableMysqlTest extends AbstractContainerDatabaseTest { - - private static final String DB_NAME = "foo"; - - private static final String USER = "bar"; - - private static final String PWD = "baz"; - - @Test - public void testSimple() 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(DB_NAME) - .withUsername(USER) - .withPassword(PWD) - .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); - } - } -} diff --git a/modules/mysql/src/test/java/org/testcontainers/junit/mysql/MultiVersionMySQLTest.java b/modules/mysql/src/test/java/org/testcontainers/mysql/MultiVersionMySQLTest.java similarity index 61% rename from modules/mysql/src/test/java/org/testcontainers/junit/mysql/MultiVersionMySQLTest.java rename to modules/mysql/src/test/java/org/testcontainers/mysql/MultiVersionMySQLTest.java index 4c8516cc5cc..8dc4454860b 100644 --- a/modules/mysql/src/test/java/org/testcontainers/junit/mysql/MultiVersionMySQLTest.java +++ b/modules/mysql/src/test/java/org/testcontainers/mysql/MultiVersionMySQLTest.java @@ -1,10 +1,8 @@ -package org.testcontainers.junit.mysql; +package org.testcontainers.mysql; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.testcontainers.MySQLTestImages; -import org.testcontainers.containers.MySQLContainer; import org.testcontainers.db.AbstractContainerDatabaseTest; import org.testcontainers.utility.DockerImageName; @@ -13,24 +11,21 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(Parameterized.class) -public class MultiVersionMySQLTest extends AbstractContainerDatabaseTest { +class MultiVersionMySQLTest extends AbstractContainerDatabaseTest { - @Parameterized.Parameters(name = "{0}") public static DockerImageName[] params() { return new DockerImageName[] { MySQLTestImages.MYSQL_57_IMAGE, MySQLTestImages.MYSQL_80_IMAGE, MySQLTestImages.MYSQL_INNOVATION_IMAGE, + MySQLTestImages.MYSQL_93_IMAGE, }; } - @Parameterized.Parameter - public DockerImageName dockerImageName; - - @Test - public void versionCheckTest() throws SQLException { - try (MySQLContainer mysql = new MySQLContainer<>(dockerImageName)) { + @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); diff --git a/modules/mysql/src/test/java/org/testcontainers/junit/mysql/SimpleMySQLTest.java b/modules/mysql/src/test/java/org/testcontainers/mysql/MySQLContainerTest.java similarity index 73% rename from modules/mysql/src/test/java/org/testcontainers/junit/mysql/SimpleMySQLTest.java rename to modules/mysql/src/test/java/org/testcontainers/mysql/MySQLContainerTest.java index 3db8da76673..ad78781db5c 100644 --- a/modules/mysql/src/test/java/org/testcontainers/junit/mysql/SimpleMySQLTest.java +++ b/modules/mysql/src/test/java/org/testcontainers/mysql/MySQLContainerTest.java @@ -1,11 +1,10 @@ -package org.testcontainers.junit.mysql; +package org.testcontainers.mysql; -import org.junit.Test; +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.MySQLContainer; import org.testcontainers.containers.output.Slf4jLogConsumer; import org.testcontainers.db.AbstractContainerDatabaseTest; @@ -27,18 +26,18 @@ import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assumptions.assumeThat; -public class SimpleMySQLTest extends AbstractContainerDatabaseTest { +class MySQLContainerTest extends AbstractContainerDatabaseTest { - private static final Logger logger = LoggerFactory.getLogger(SimpleMySQLTest.class); + private static final Logger logger = LoggerFactory.getLogger(MySQLContainerTest.class); @Test - public void testSimple() throws SQLException { - try ( - MySQLContainer mysql = new MySQLContainer<>(MySQLTestImages.MYSQL_80_IMAGE) - .withLogConsumer(new Slf4jLogConsumer(logger)) + void testSimple() throws SQLException { + try ( // container { + MySQLContainer mysql = new MySQLContainer("mysql:8.0.36") + // } ) { mysql.start(); @@ -51,9 +50,9 @@ public void testSimple() throws SQLException { } @Test - public void testSpecificVersion() throws SQLException { + void testSpecificVersion() throws SQLException { try ( - MySQLContainer mysqlOldVersion = new MySQLContainer<>(MySQLTestImages.MYSQL_80_IMAGE) + MySQLContainer mysqlOldVersion = new MySQLContainer(MySQLTestImages.MYSQL_80_IMAGE) .withConfigurationOverride("somepath/mysql_conf_override") .withLogConsumer(new Slf4jLogConsumer(logger)) ) { @@ -69,9 +68,9 @@ public void testSpecificVersion() throws SQLException { } @Test - public void testMySQLWithCustomIniFile() throws SQLException { + void testMySQLWithCustomIniFile() throws SQLException { try ( - MySQLContainer mysqlCustomConfig = new MySQLContainer<>(MySQLTestImages.MYSQL_80_IMAGE) + MySQLContainer mysqlCustomConfig = new MySQLContainer(MySQLTestImages.MYSQL_80_IMAGE) .withConfigurationOverride("somepath/mysql_conf_override") ) { mysqlCustomConfig.start(); @@ -81,9 +80,9 @@ public void testMySQLWithCustomIniFile() throws SQLException { } @Test - public void testCommandOverride() throws SQLException { + void testCommandOverride() throws SQLException { try ( - MySQLContainer mysqlCustomConfig = new MySQLContainer<>(MySQLTestImages.MYSQL_80_IMAGE) + MySQLContainer mysqlCustomConfig = new MySQLContainer(MySQLTestImages.MYSQL_80_IMAGE) .withCommand("mysqld --auto_increment_increment=42") ) { mysqlCustomConfig.start(); @@ -91,14 +90,14 @@ public void testCommandOverride() throws SQLException { ResultSet resultSet = performQuery(mysqlCustomConfig, "show variables like 'auto_increment_increment'"); String result = resultSet.getString("Value"); - assertThat(result).as("Auto increment increment should be overriden by command line").isEqualTo("42"); + assertThat(result).as("Auto increment increment should be overridden by command line").isEqualTo("42"); } } @Test - public void testExplicitInitScript() throws SQLException { + void testExplicitInitScript() throws SQLException { try ( - MySQLContainer container = new MySQLContainer<>(MySQLTestImages.MYSQL_80_IMAGE) + MySQLContainer container = new MySQLContainer(MySQLTestImages.MYSQL_80_IMAGE) .withInitScript("somepath/init_mysql.sql") .withLogConsumer(new Slf4jLogConsumer(logger)) ) { @@ -111,25 +110,26 @@ public void testExplicitInitScript() throws SQLException { } } - @Test(expected = ContainerLaunchException.class) - public void testEmptyPasswordWithNonRootUser() { + @Test + void testEmptyPasswordWithNonRootUser() { try ( - MySQLContainer container = new MySQLContainer<>(MySQLTestImages.MYSQL_80_IMAGE) + MySQLContainer container = new MySQLContainer(MySQLTestImages.MYSQL_80_IMAGE) .withDatabaseName("TEST") .withUsername("test") .withPassword("") .withEnv("MYSQL_ROOT_HOST", "%") ) { - container.start(); - fail("ContainerLaunchException expected to be thrown"); + assertThatThrownBy(container::start) + .isInstanceOf(ContainerLaunchException.class) + .hasMessageStartingWith("Container startup failed for image mysql"); } } @Test - public void testEmptyPasswordWithRootUser() throws SQLException { + 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) + MySQLContainer mysql = new MySQLContainer(MySQLTestImages.MYSQL_80_IMAGE) .withDatabaseName("foo") .withUsername("root") .withPassword("") @@ -145,8 +145,8 @@ public void testEmptyPasswordWithRootUser() throws SQLException { } @Test - public void testWithAdditionalUrlParamTimeZone() throws SQLException { - MySQLContainer mysql = new MySQLContainer<>(MySQLTestImages.MYSQL_80_IMAGE) + void testWithAdditionalUrlParamTimeZone() throws SQLException { + MySQLContainer mysql = new MySQLContainer(MySQLTestImages.MYSQL_80_IMAGE) .withUrlParam("serverTimezone", "Europe/Zurich") .withEnv("TZ", "Europe/Zurich") .withLogConsumer(new Slf4jLogConsumer(logger)); @@ -180,8 +180,8 @@ public void testWithAdditionalUrlParamTimeZone() throws SQLException { } @Test - public void testWithAdditionalUrlParamMultiQueries() throws SQLException { - MySQLContainer mysql = new MySQLContainer<>(MySQLTestImages.MYSQL_80_IMAGE) + void testWithAdditionalUrlParamMultiQueries() throws SQLException { + MySQLContainer mysql = new MySQLContainer(MySQLTestImages.MYSQL_80_IMAGE) .withUrlParam("allowMultiQueries", "true") .withLogConsumer(new Slf4jLogConsumer(logger)); mysql.start(); @@ -205,8 +205,8 @@ public void testWithAdditionalUrlParamMultiQueries() throws SQLException { } @Test - public void testWithAdditionalUrlParamInJdbcUrl() { - MySQLContainer mysql = new MySQLContainer<>(MySQLTestImages.MYSQL_80_IMAGE) + void testWithAdditionalUrlParamInJdbcUrl() { + MySQLContainer mysql = new MySQLContainer(MySQLTestImages.MYSQL_80_IMAGE) .withUrlParam("allowMultiQueries", "true") .withUrlParam("rewriteBatchedStatements", "true") .withLogConsumer(new Slf4jLogConsumer(logger)); @@ -224,10 +224,10 @@ public void testWithAdditionalUrlParamInJdbcUrl() { } @Test - public void testWithOnlyUserReadableCustomIniFile() throws Exception { + void testWithOnlyUserReadableCustomIniFile() throws Exception { assumeThat(FileSystems.getDefault().supportedFileAttributeViews().contains("posix")).isTrue(); try ( - MySQLContainer mysql = new MySQLContainer<>(MySQLTestImages.MYSQL_80_IMAGE) + MySQLContainer mysql = new MySQLContainer(MySQLTestImages.MYSQL_80_IMAGE) .withConfigurationOverride("somepath/mysql_conf_override") .withLogConsumer(new Slf4jLogConsumer(logger)) ) { @@ -251,12 +251,31 @@ public void testWithOnlyUserReadableCustomIniFile() throws Exception { } } - private void assertHasCorrectExposedAndLivenessCheckPorts(MySQLContainer 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 { + 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) diff --git a/modules/mysql/src/test/java/org/testcontainers/mysql/MySQLR2DBCDatabaseContainerTest.java b/modules/mysql/src/test/java/org/testcontainers/mysql/MySQLR2DBCDatabaseContainerTest.java new file mode 100644 index 00000000000..5e86a92347b --- /dev/null +++ b/modules/mysql/src/test/java/org/testcontainers/mysql/MySQLR2DBCDatabaseContainerTest.java @@ -0,0 +1,23 @@ +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); + } +} diff --git a/modules/mysql/src/test/resources/somepath/mysql_conf_override/my.cnf b/modules/mysql/src/test/resources/somepath/mysql_conf_override/my.cnf index 5721bf52ccb..6e6dcd28ad1 100644 --- a/modules/mysql/src/test/resources/somepath/mysql_conf_override/my.cnf +++ b/modules/mysql/src/test/resources/somepath/mysql_conf_override/my.cnf @@ -46,7 +46,6 @@ innodb_data_file_path = ibdata1:10M:autoextend 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 diff --git a/modules/neo4j/build.gradle b/modules/neo4j/build.gradle index 8619cbc6940..96f73b3bb81 100644 --- a/modules/neo4j/build.gradle +++ b/modules/neo4j/build.gradle @@ -33,6 +33,5 @@ dependencies { api project(":testcontainers") - testImplementation 'org.neo4j.driver:neo4j-java-driver:4.4.13' - testImplementation 'org.assertj:assertj-core:3.25.2' + testImplementation 'org.neo4j.driver:neo4j-java-driver:4.4.22' } diff --git a/modules/neo4j/src/main/java/org/testcontainers/containers/Neo4jContainer.java b/modules/neo4j/src/main/java/org/testcontainers/containers/Neo4jContainer.java index 30fd6b8397c..f7fc0beacfe 100644 --- a/modules/neo4j/src/main/java/org/testcontainers/containers/Neo4jContainer.java +++ b/modules/neo4j/src/main/java/org/testcontainers/containers/Neo4jContainer.java @@ -30,7 +30,10 @@ *

  • HTTP: 7474
  • *
  • HTTPS: 7473
  • * + * + * @deprecated use {@link org.testcontainers.neo4j.Neo4jContainer} instead. */ +@Deprecated public class Neo4jContainer> extends GenericContainer { /** @@ -317,29 +320,18 @@ public String getAdminPassword() { } /** - * Registers one or more {@link Neo4jLabsPlugin} for download and server startup. - - * @param neo4jLabsPlugins The Neo4j plugins that should get started with the server. - * @return This container. - */ - public S withLabsPlugins(Neo4jLabsPlugin... neo4jLabsPlugins) { - List pluginNames = Arrays - .stream(neo4jLabsPlugins) - .map(plugin -> plugin.pluginName) - .collect(Collectors.toList()); - - this.labsPlugins.addAll(pluginNames); - return self(); - } - - /** - * Registers one or more {@link Neo4jLabsPlugin} for download and server startup. - - * @param neo4jLabsPlugins The Neo4j plugins that should get started with the server. + * 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 withLabsPlugins(String... neo4jLabsPlugins) { - this.labsPlugins.addAll(Arrays.asList(neo4jLabsPlugins)); + public S withPlugins(String... plugins) { + this.labsPlugins.addAll(Arrays.asList(plugins)); return self(); } diff --git a/modules/neo4j/src/main/java/org/testcontainers/containers/Neo4jLabsPlugin.java b/modules/neo4j/src/main/java/org/testcontainers/containers/Neo4jLabsPlugin.java deleted file mode 100644 index 96c8e987c85..00000000000 --- a/modules/neo4j/src/main/java/org/testcontainers/containers/Neo4jLabsPlugin.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.testcontainers.containers; - -/** - * Reflects a plugin from the official Neo4j 4.4. - * Neo4j Labs Plugin list. - * There might be plugins not supported by your selected version of Neo4j. - */ -public enum Neo4jLabsPlugin { - APOC("apoc"), - APOC_CORE("apoc-core"), - BLOOM("bloom"), - STREAMS("streams"), - GRAPH_DATA_SCIENCE("graph-data-science"), - NEO_SEMANTICS("n10s"); - - final String pluginName; - - Neo4jLabsPlugin(String pluginName) { - this.pluginName = pluginName; - } -} diff --git a/modules/neo4j/src/main/java/org/testcontainers/neo4j/Neo4jContainer.java b/modules/neo4j/src/main/java/org/testcontainers/neo4j/Neo4jContainer.java new file mode 100644 index 00000000000..0349e0c2395 --- /dev/null +++ b/modules/neo4j/src/main/java/org/testcontainers/neo4j/Neo4jContainer.java @@ -0,0 +1,331 @@ +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()); + } +} diff --git a/modules/neo4j/src/test/java/org/testcontainers/containers/Neo4jContainerJUnitIntegrationTest.java b/modules/neo4j/src/test/java/org/testcontainers/containers/Neo4jContainerJUnitIntegrationTest.java deleted file mode 100644 index b5f9c2ad867..00000000000 --- a/modules/neo4j/src/test/java/org/testcontainers/containers/Neo4jContainerJUnitIntegrationTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.testcontainers.containers; - -import org.junit.ClassRule; -import org.junit.Test; -import org.neo4j.driver.AuthTokens; -import org.neo4j.driver.Driver; -import org.neo4j.driver.GraphDatabase; -import org.neo4j.driver.Session; - -import java.util.Collections; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - -/** - * Test for basic functionality when used as a @ClassRule. - */ -public class Neo4jContainerJUnitIntegrationTest { - - @ClassRule - public static Neo4jContainer neo4jContainer = new Neo4jContainer<>("neo4j:4.4"); - - @Test - public void shouldStart() { - boolean actual = neo4jContainer.isRunning(); - assertThat(actual).isTrue(); - - try ( - Driver driver = GraphDatabase.driver(neo4jContainer.getBoltUrl(), AuthTokens.basic("neo4j", "password")); - 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 - public void shouldReturnBoltUrl() { - String actual = neo4jContainer.getBoltUrl(); - - assertThat(actual).isNotNull(); - assertThat(actual).startsWith("bolt://"); - } - - @Test - public void shouldReturnHttpUrl() { - String actual = neo4jContainer.getHttpUrl(); - - assertThat(actual).isNotNull(); - assertThat(actual).startsWith("http://"); - } -} diff --git a/modules/neo4j/src/test/java/org/testcontainers/containers/Neo4jContainerTest.java b/modules/neo4j/src/test/java/org/testcontainers/containers/Neo4jContainerTest.java deleted file mode 100644 index 5341b10c599..00000000000 --- a/modules/neo4j/src/test/java/org/testcontainers/containers/Neo4jContainerTest.java +++ /dev/null @@ -1,385 +0,0 @@ -package org.testcontainers.containers; - -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.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.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatNoException; -import static org.assertj.core.api.Assumptions.assumeThat; - -/** - * Tests of functionality special to the Neo4jContainer. - */ -public class Neo4jContainerTest { - - // See org.testcontainers.utility.LicenseAcceptance#ACCEPTANCE_FILE_NAME - private static final String ACCEPTANCE_FILE_LOCATION = "/container-license-acceptance.txt"; - - @Test - public 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 - public 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 - public void shouldFailOnCopyDatabaseForDefaultNeo4j4Image() { - assertThatIllegalArgumentException() - .isThrownBy(() -> new Neo4jContainer<>().withDatabase(MountableFile.forClasspathResource("/test-graph.db"))) - .withMessage("Copying database folder is not supported for Neo4j instances with version 4.0 or higher."); - } - - @Test - public 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 - public 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 - public 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 - public 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 - public void shouldCheckEnterpriseLicense() { - assumeThat(Neo4jContainerTest.class.getResource(ACCEPTANCE_FILE_LOCATION)).isNull(); - - String expectedImageName = "neo4j:4.4-enterprise"; - - assertThatExceptionOfType(IllegalStateException.class) - .isThrownBy(() -> new Neo4jContainer<>("neo4j:4.4").withEnterpriseEdition()) - .withMessageContaining("The image " + expectedImageName + " requires you to accept a license agreement."); - } - - @Test - public void shouldRunEnterprise() { - assumeThat(Neo4jContainerTest.class.getResource(ACCEPTANCE_FILE_LOCATION)).isNotNull(); - - try ( - // enterpriseEdition { - Neo4jContainer neo4jContainer = new Neo4jContainer<>("neo4j:4.4") - .withEnterpriseEdition() - // } - .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 - public 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 - public 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 - public void shouldSetCustomPasswordCorrectly() { - // withoutAuthentication { - Neo4jContainer neo4jContainer = new Neo4jContainer<>("neo4j:4.4").withAdminPassword("verySecret"); - // } - - neo4jContainer.configure(); - - assertThat(neo4jContainer.getEnvMap()).containsEntry("NEO4J_AUTH", "neo4j/verySecret"); - } - - @Test - public 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 - public 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 - public void shouldRespectAlreadyDefinedPortMappingsBolt() { - Neo4jContainer neo4jContainer = new Neo4jContainer<>("neo4j:4.4").withExposedPorts(7687); - - neo4jContainer.configure(); - - assertThat(neo4jContainer.getExposedPorts()).containsExactly(7687); - } - - @Test - public void shouldRespectAlreadyDefinedPortMappingsHttp() { - Neo4jContainer neo4jContainer = new Neo4jContainer<>("neo4j:4.4").withExposedPorts(7474); - - neo4jContainer.configure(); - - assertThat(neo4jContainer.getExposedPorts()).containsExactly(7474); - } - - @Test - public void shouldRespectAlreadyDefinedPortMappingsWithoutHttps() { - Neo4jContainer neo4jContainer = new Neo4jContainer<>("neo4j:4.4").withExposedPorts(7687, 7474); - - neo4jContainer.configure(); - - assertThat(neo4jContainer.getExposedPorts()).containsExactlyInAnyOrder(7474, 7687); - } - - @Test - public void shouldDefaultExportBoltHttpAndHttps() { - Neo4jContainer neo4jContainer = new Neo4jContainer<>("neo4j:4.4"); - - neo4jContainer.configure(); - - assertThat(neo4jContainer.getExposedPorts()).containsExactlyInAnyOrder(7473, 7474, 7687); - } - - @Test - public void shouldRespectCustomWaitStrategy() { - Neo4jContainer neo4jContainer = new Neo4jContainer<>("neo4j:4.4").waitingFor(new CustomDummyWaitStrategy()); - - neo4jContainer.configure(); - - assertThat(neo4jContainer.getWaitStrategy()).isInstanceOf(CustomDummyWaitStrategy.class); - } - - @Test - public void shouldConfigureSingleLabsPlugin() { - try ( - Neo4jContainer neo4jContainer = new Neo4jContainer<>("neo4j:4.4").withLabsPlugins(Neo4jLabsPlugin.APOC) - ) { - // needs to get called explicitly for setup - neo4jContainer.configure(); - - assertThat(neo4jContainer.getEnvMap()).containsEntry("NEO4JLABS_PLUGINS", "[\"apoc\"]"); - } - } - - @Test - public void shouldConfigureMultipleLabsPlugins() { - try ( - // configureLabsPlugins { - Neo4jContainer neo4jContainer = new Neo4jContainer<>("neo4j:4.4") - .withLabsPlugins(Neo4jLabsPlugin.APOC, Neo4jLabsPlugin.BLOOM); - // } - ) { - // needs to get called explicitly for setup - neo4jContainer.configure(); - - assertThat(neo4jContainer.getEnvMap().get("NEO4JLABS_PLUGINS")) - .containsAnyOf("[\"apoc\",\"bloom\"]", "[\"bloom\",\"apoc\"]"); - } - } - - @Test - public void shouldConfigureSingleLabsPluginWithString() { - try (Neo4jContainer neo4jContainer = new Neo4jContainer<>("neo4j:4.4").withLabsPlugins("myApoc")) { - // needs to get called explicitly for setup - neo4jContainer.configure(); - - assertThat(neo4jContainer.getEnvMap()).containsEntry("NEO4JLABS_PLUGINS", "[\"myApoc\"]"); - } - } - - @Test - public void shouldConfigureMultipleLabsPluginsWithString() { - try ( - Neo4jContainer neo4jContainer = new Neo4jContainer<>("neo4j:4.4").withLabsPlugins("myApoc", "myBloom") - ) { - // needs to get called explicitly for setup - neo4jContainer.configure(); - - assertThat(neo4jContainer.getEnvMap().get("NEO4JLABS_PLUGINS")) - .containsAnyOf("[\"myApoc\",\"myBloom\"]", "[\"myBloom\",\"myApoc\"]"); - } - } - - @Test - public 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 - public 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); - } -} diff --git a/modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java b/modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java new file mode 100644 index 00000000000..b880e7b4e8f --- /dev/null +++ b/modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java @@ -0,0 +1,361 @@ +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 authenticated() { + try ( + // container { + Neo4jContainer neo4j = new Neo4jContainer("neo4j:4.4") + // } + ) { + neo4j.start(); + try (Driver driver = getDriver(neo4j); Session session = driver.session()) { + long one = session.run("RETURN 1", Collections.emptyMap()).next().get(0).asLong(); + assertThat(one).isEqualTo(1L); + } + } + } + + @Test + void shouldDisableAuthentication() { + try ( + // spotless:off + // withoutAuthentication { + Neo4jContainer neo4j = new Neo4jContainer("neo4j:4.4") + .withoutAuthentication() + // } + // spotless:on + ) { + neo4j.start(); + try (Driver driver = getDriver(neo4j); 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 neo4j = new Neo4jContainer("neo4j:3.5.30") + .withDatabase(MountableFile.forClasspathResource("/test-graph.db")) + // } + ) { + neo4j.start(); + try (Driver driver = getDriver(neo4j); 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 neo4j = new Neo4jContainer("neo4j:4.4") + .withPlugins(MountableFile.forClasspathResource("/custom-plugins")) + // } + ) { + neo4j.start(); + try (Driver driver = getDriver(neo4j); Session session = driver.session()) { + assertThatCustomPluginWasCopied(session); + } + } + } + + @Test + void shouldCopyPlugin() { + try ( + // registerPluginsJar { + Neo4jContainer neo4j = new Neo4jContainer("neo4j:4.4") + .withPlugins(MountableFile.forClasspathResource("/custom-plugins/hello-world.jar")) + // } + ) { + neo4j.start(); + try (Driver driver = getDriver(neo4j); 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 neo4j = new Neo4jContainer("neo4j:4.4-enterprise") + .acceptLicense() + // } + .withAdminPassword("Picard123") + ) { + neo4j.start(); + try (Driver driver = getDriver(neo4j); 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 neo4j = new Neo4jContainer("neo4j:4.4") + .withNeo4jConfig("dbms.security.procedures.unrestricted", "apoc.*,algo.*") + .withNeo4jConfig("dbms.tx_log.rotation.size", "42M"); + // } + + assertThat(neo4j.getEnvMap()).containsEntry("NEO4J_dbms_security_procedures_unrestricted", "apoc.*,algo.*"); + assertThat(neo4j.getEnvMap()).containsEntry("NEO4J_dbms_tx__log_rotation_size", "42M"); + } + + @Test + void shouldRespectEnvironmentAuth() { + Neo4jContainer neo4j = new Neo4jContainer("neo4j:4.4").withEnv("NEO4J_AUTH", "neo4j/secret"); + + neo4j.configure(); + + assertThat(neo4j.getEnvMap()).containsEntry("NEO4J_AUTH", "neo4j/secret"); + } + + @Test + void shouldSetCustomPasswordCorrectly() { + // withAdminPassword { + Neo4jContainer neo4j = new Neo4jContainer("neo4j:4.4").withAdminPassword("verySecret"); + // } + + neo4j.configure(); + + assertThat(neo4j.getEnvMap()).containsEntry("NEO4J_AUTH", "neo4j/verySecret"); + } + + @Test + void adminPasswordOverrulesEnvironmentAuth() { + Neo4jContainer neo4j = new Neo4jContainer("neo4j:4.4") + .withEnv("NEO4J_AUTH", "neo4j/secret") + .withAdminPassword("anotherSecret"); + + neo4j.configure(); + + assertThat(neo4j.getEnvMap()).containsEntry("NEO4J_AUTH", "neo4j/anotherSecret"); + } + + @Test + void shouldWithoutAuthenticationOverrulesEnvironmentAuth() { + Neo4jContainer neo4j = new Neo4jContainer("neo4j:4.4") + .withEnv("NEO4J_AUTH", "neo4j/secret") + .withoutAuthentication(); + + neo4j.configure(); + + assertThat(neo4j.getEnvMap()).containsEntry("NEO4J_AUTH", "none"); + } + + @Test + void shouldRespectAlreadyDefinedPortMappingsBolt() { + Neo4jContainer neo4j = new Neo4jContainer("neo4j:4.4").withExposedPorts(7687); + + neo4j.configure(); + + assertThat(neo4j.getExposedPorts()).containsExactly(7687); + } + + @Test + void shouldRespectAlreadyDefinedPortMappingsHttp() { + Neo4jContainer neo4j = new Neo4jContainer("neo4j:4.4").withExposedPorts(7474); + + neo4j.configure(); + + assertThat(neo4j.getExposedPorts()).containsExactly(7474); + } + + @Test + void shouldRespectAlreadyDefinedPortMappingsWithoutHttps() { + Neo4jContainer neo4j = new Neo4jContainer("neo4j:4.4").withExposedPorts(7687, 7474); + + neo4j.configure(); + + assertThat(neo4j.getExposedPorts()).containsExactlyInAnyOrder(7474, 7687); + } + + @Test + void shouldDefaultExportBoltHttpAndHttps() { + Neo4jContainer neo4j = new Neo4jContainer("neo4j:4.4"); + + neo4j.configure(); + + assertThat(neo4j.getExposedPorts()).containsExactlyInAnyOrder(7473, 7474, 7687); + } + + @Test + void shouldRespectCustomWaitStrategy() { + Neo4jContainer neo4j = new Neo4jContainer("neo4j:4.4").waitingFor(new CustomDummyWaitStrategy()); + + neo4j.configure(); + + assertThat(neo4j).extracting("waitStrategy").isInstanceOf(CustomDummyWaitStrategy.class); + } + + @Test + void shouldConfigureSinglePluginByName() { + try (Neo4jContainer neo4j = new Neo4jContainer("neo4j:4.4").withPlugins("apoc")) { + // needs to get called explicitly for setup + neo4j.configure(); + + assertThat(neo4j.getEnvMap()).containsEntry("NEO4JLABS_PLUGINS", "[\"apoc\"]"); + } + } + + @Test + void shouldConfigureMultiplePluginsByName() { + try ( + // configureLabsPlugins { + Neo4jContainer neo4j = new Neo4jContainer("neo4j:4.4") // + .withPlugins("apoc", "bloom"); + // } + ) { + // needs to get called explicitly for setup + neo4j.configure(); + + assertThat(neo4j.getEnvMap().get("NEO4JLABS_PLUGINS")) + .containsAnyOf("[\"apoc\",\"bloom\"]", "[\"bloom\",\"apoc\"]"); + } + } + + @Test + void shouldCreateRandomUuidBasedPasswords() { + try ( + // withRandomPassword { + Neo4jContainer neo4j = new Neo4jContainer("neo4j:4.4").withRandomPassword(); + // } + ) { + // It will throw an exception if it's not UUID parsable. + assertThatNoException().isThrownBy(neo4j::configure); + // This basically is always true at if the random password is UUID-like. + assertThat(neo4j.getAdminPassword()) + .satisfies(password -> assertThat(UUID.fromString(password).toString()).isEqualTo(password)); + } + } + + @Test + void shouldWarnOnPasswordTooShort() { + try (Neo4jContainer neo4j = new Neo4jContainer("neo4j:4.4");) { + Logger logger = (Logger) DockerLoggerFactory.getLogger("neo4j:4.4"); + TestLogAppender testLogAppender = new TestLogAppender(); + logger.addAppender(testLogAppender); + testLogAppender.start(); + + neo4j.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 neo4j) { + AuthToken authToken = AuthTokens.none(); + if (neo4j.getAdminPassword() != null) { + authToken = AuthTokens.basic("neo4j", neo4j.getAdminPassword()); + } + return GraphDatabase.driver(neo4j.getBoltUrl(), authToken); + } +} diff --git a/modules/nginx/build.gradle b/modules/nginx/build.gradle index 406fc608f08..60d24d863d8 100644 --- a/modules/nginx/build.gradle +++ b/modules/nginx/build.gradle @@ -2,6 +2,5 @@ description = "Testcontainers :: Nginx" dependencies { api project(':testcontainers') - compileOnly 'org.jetbrains:annotations:24.1.0' - testImplementation 'org.assertj:assertj-core:3.25.1' + compileOnly 'org.jetbrains:annotations:26.1.0' } diff --git a/modules/nginx/src/main/java/org/testcontainers/containers/NginxContainer.java b/modules/nginx/src/main/java/org/testcontainers/containers/NginxContainer.java index 9a8687aaf8c..c66149d2417 100644 --- a/modules/nginx/src/main/java/org/testcontainers/containers/NginxContainer.java +++ b/modules/nginx/src/main/java/org/testcontainers/containers/NginxContainer.java @@ -8,6 +8,10 @@ import java.net.URL; import java.util.Set; +/** + * @deprecated use {@link org.testcontainers.nginx.NginxContainer} instead. + */ +@Deprecated public class NginxContainer> extends GenericContainer implements LinkableContainer { diff --git a/modules/nginx/src/main/java/org/testcontainers/nginx/NginxContainer.java b/modules/nginx/src/main/java/org/testcontainers/nginx/NginxContainer.java new file mode 100644 index 00000000000..f6eb70814ac --- /dev/null +++ b/modules/nginx/src/main/java/org/testcontainers/nginx/NginxContainer.java @@ -0,0 +1,34 @@ +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); + } +} diff --git a/modules/nginx/src/test/java/org/testcontainers/junit/SimpleNginxTest.java b/modules/nginx/src/test/java/org/testcontainers/nginx/NginxContainerTest.java similarity index 63% rename from modules/nginx/src/test/java/org/testcontainers/junit/SimpleNginxTest.java rename to modules/nginx/src/test/java/org/testcontainers/nginx/NginxContainerTest.java index 6b1bcf55d5b..ec495a06d1c 100644 --- a/modules/nginx/src/test/java/org/testcontainers/junit/SimpleNginxTest.java +++ b/modules/nginx/src/test/java/org/testcontainers/nginx/NginxContainerTest.java @@ -1,10 +1,8 @@ -package org.testcontainers.junit; +package org.testcontainers.nginx; import lombok.Cleanup; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Test; -import org.testcontainers.containers.NginxContainer; +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; @@ -20,23 +18,15 @@ import static org.assertj.core.api.Assertions.assertThat; -public class SimpleNginxTest { +class NginxContainerTest { - private static final DockerImageName NGINX_IMAGE = DockerImageName.parse("nginx:1.9.4"); + 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"; - // creatingContainer { - @Rule - public NginxContainer nginx = new NginxContainer<>(NGINX_IMAGE) - .withCopyFileToContainer(MountableFile.forHostPath(tmpDirectory), "/usr/share/nginx/html") - .waitingFor(new HttpWaitStrategy()); - - // } - @SuppressWarnings({ "Duplicates", "ResultOfMethodCallIgnored" }) - @BeforeClass - public static void setupContent() throws Exception { + @BeforeAll + static void setupContent() throws Exception { // addCustomContent { // Create a temporary dir File contentFolder = new File(tmpDirectory); @@ -53,18 +43,27 @@ public static void setupContent() throws Exception { } @Test - public void testSimple() throws Exception { - // getFromNginxServer { - URL baseUrl = nginx.getBaseUrl("http", 80); + 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); + 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) throws Exception { + private void assertHasCorrectExposedAndLivenessCheckPorts(NginxContainer nginxContainer) { assertThat(nginxContainer.getExposedPorts()).containsExactly(80); assertThat(nginxContainer.getLivenessCheckPortNumbers()).containsExactly(nginxContainer.getMappedPort(80)); } diff --git a/modules/oceanbase/build.gradle b/modules/oceanbase/build.gradle new file mode 100644 index 00000000000..f570d741f7b --- /dev/null +++ b/modules/oceanbase/build.gradle @@ -0,0 +1,8 @@ +description = "Testcontainers :: JDBC :: OceanBase" + +dependencies { + api project(':testcontainers-jdbc') + + testImplementation project(':testcontainers-jdbc-test') + testRuntimeOnly 'com.mysql:mysql-connector-j:9.6.0' +} diff --git a/modules/oceanbase/src/main/java/org/testcontainers/oceanbase/OceanBaseCEContainer.java b/modules/oceanbase/src/main/java/org/testcontainers/oceanbase/OceanBaseCEContainer.java new file mode 100644 index 00000000000..fae9190ceb0 --- /dev/null +++ b/modules/oceanbase/src/main/java/org/testcontainers/oceanbase/OceanBaseCEContainer.java @@ -0,0 +1,142 @@ +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, + } +} diff --git a/modules/oceanbase/src/main/java/org/testcontainers/oceanbase/OceanBaseCEContainerProvider.java b/modules/oceanbase/src/main/java/org/testcontainers/oceanbase/OceanBaseCEContainerProvider.java new file mode 100644 index 00000000000..2a62558b6c4 --- /dev/null +++ b/modules/oceanbase/src/main/java/org/testcontainers/oceanbase/OceanBaseCEContainerProvider.java @@ -0,0 +1,32 @@ +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(); + } + } +} diff --git a/modules/oceanbase/src/main/java/org/testcontainers/oceanbase/OceanBaseJdbcUtils.java b/modules/oceanbase/src/main/java/org/testcontainers/oceanbase/OceanBaseJdbcUtils.java new file mode 100644 index 00000000000..d2e90f48ee6 --- /dev/null +++ b/modules/oceanbase/src/main/java/org/testcontainers/oceanbase/OceanBaseJdbcUtils.java @@ -0,0 +1,45 @@ +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); + } +} diff --git a/modules/oceanbase/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider b/modules/oceanbase/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider new file mode 100644 index 00000000000..505bfe5e088 --- /dev/null +++ b/modules/oceanbase/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider @@ -0,0 +1 @@ +org.testcontainers.oceanbase.OceanBaseCEContainerProvider diff --git a/modules/oceanbase/src/test/java/org/testcontainers/oceanbase/OceanBaseJdbcDriverTest.java b/modules/oceanbase/src/test/java/org/testcontainers/oceanbase/OceanBaseJdbcDriverTest.java new file mode 100644 index 00000000000..b701d953e9a --- /dev/null +++ b/modules/oceanbase/src/test/java/org/testcontainers/oceanbase/OceanBaseJdbcDriverTest.java @@ -0,0 +1,15 @@ +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) } } + ); + } +} diff --git a/modules/oceanbase/src/test/java/org/testcontainers/oceanbase/SimpleOceanBaseCETest.java b/modules/oceanbase/src/test/java/org/testcontainers/oceanbase/SimpleOceanBaseCETest.java new file mode 100644 index 00000000000..ee2d8196718 --- /dev/null +++ b/modules/oceanbase/src/test/java/org/testcontainers/oceanbase/SimpleOceanBaseCETest.java @@ -0,0 +1,62 @@ +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)); + } +} diff --git a/modules/oceanbase/src/test/resources/init.sql b/modules/oceanbase/src/test/resources/init.sql new file mode 100644 index 00000000000..98d6889b078 --- /dev/null +++ b/modules/oceanbase/src/test/resources/init.sql @@ -0,0 +1,45 @@ +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'); diff --git a/modules/oceanbase/src/test/resources/logback-test.xml b/modules/oceanbase/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..83ef7a1a3ef --- /dev/null +++ b/modules/oceanbase/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + diff --git a/modules/ollama/build.gradle b/modules/ollama/build.gradle new file mode 100644 index 00000000000..0da01d0ad96 --- /dev/null +++ b/modules/ollama/build.gradle @@ -0,0 +1,7 @@ +description = "Testcontainers :: Ollama" + +dependencies { + api project(':testcontainers') + + testImplementation 'io.rest-assured:rest-assured:5.5.7' +} diff --git a/modules/ollama/src/main/java/org/testcontainers/ollama/OllamaContainer.java b/modules/ollama/src/main/java/org/testcontainers/ollama/OllamaContainer.java new file mode 100644 index 00000000000..db611a34a39 --- /dev/null +++ b/modules/ollama/src/main/java/org/testcontainers/ollama/OllamaContainer.java @@ -0,0 +1,86 @@ +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(); + } +} diff --git a/modules/ollama/src/test/java/org/testcontainers/ollama/OllamaContainerTest.java b/modules/ollama/src/test/java/org/testcontainers/ollama/OllamaContainerTest.java new file mode 100644 index 00000000000..dcd61cad9c4 --- /dev/null +++ b/modules/ollama/src/test/java/org/testcontainers/ollama/OllamaContainerTest.java @@ -0,0 +1,65 @@ +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"); + } + } +} diff --git a/modules/ollama/src/test/resources/logback-test.xml b/modules/ollama/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..83ef7a1a3ef --- /dev/null +++ b/modules/ollama/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + diff --git a/modules/openfga/build.gradle b/modules/openfga/build.gradle new file mode 100644 index 00000000000..b6bf9ff8e70 --- /dev/null +++ b/modules/openfga/build.gradle @@ -0,0 +1,20 @@ +description = "Testcontainers :: OpenFGA" + +dependencies { + api project(':testcontainers') + + testImplementation 'dev.openfga:openfga-sdk:0.9.9' +} + +test { + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(17) + } +} + +compileTestJava { + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(17) + } + options.release.set(17) +} diff --git a/modules/openfga/src/main/java/org/testcontainers/openfga/OpenFGAContainer.java b/modules/openfga/src/main/java/org/testcontainers/openfga/OpenFGAContainer.java new file mode 100644 index 00000000000..986e385c0d3 --- /dev/null +++ b/modules/openfga/src/main/java/org/testcontainers/openfga/OpenFGAContainer.java @@ -0,0 +1,45 @@ +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); + } +} diff --git a/modules/openfga/src/test/java/org/testcontainers/openfga/OpenFGAContainerTest.java b/modules/openfga/src/test/java/org/testcontainers/openfga/OpenFGAContainerTest.java new file mode 100644 index 00000000000..be68bdee71a --- /dev/null +++ b/modules/openfga/src/test/java/org/testcontainers/openfga/OpenFGAContainerTest.java @@ -0,0 +1,33 @@ +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); + } + } +} diff --git a/modules/openfga/src/test/resources/logback-test.xml b/modules/openfga/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..83ef7a1a3ef --- /dev/null +++ b/modules/openfga/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + diff --git a/modules/oracle-free/build.gradle b/modules/oracle-free/build.gradle index d2123af02f3..935fe701f6f 100644 --- a/modules/oracle-free/build.gradle +++ b/modules/oracle-free/build.gradle @@ -1,32 +1,16 @@ description = "Testcontainers :: JDBC :: Oracle Database Free" dependencies { - annotationProcessor 'com.google.auto.service:auto-service:1.1.1' - compileOnly 'com.google.auto.service:auto-service:1.1.1' + api project(':testcontainers-jdbc') - api project(':jdbc') + compileOnly project(':testcontainers-r2dbc') + compileOnly 'com.oracle.database.r2dbc:oracle-r2dbc:1.3.0' - compileOnly project(':r2dbc') - compileOnly 'com.oracle.database.r2dbc:oracle-r2dbc:1.2.0' + testImplementation project(':testcontainers-jdbc-test') + testImplementation 'com.oracle.database.jdbc:ojdbc11:23.26.2.0.0' - testImplementation project(':jdbc-test') - testImplementation 'com.oracle.database.jdbc:ojdbc11:23.3.0.23.09' + compileOnly 'org.jetbrains:annotations:26.1.0' - compileOnly 'org.jetbrains:annotations:24.1.0' - - testImplementation testFixtures(project(':r2dbc')) - testRuntimeOnly 'com.oracle.database.r2dbc:oracle-r2dbc:1.2.0' -} - -test { - javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(11) - } -} - -compileTestJava { - javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(11) - } - options.release.set(11) + testImplementation testFixtures(project(':testcontainers-r2dbc')) + testRuntimeOnly 'com.oracle.database.r2dbc:oracle-r2dbc:1.3.0' } diff --git a/modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleContainer.java b/modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleContainer.java index 1130072be12..2080c24283b 100644 --- a/modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleContainer.java +++ b/modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleContainer.java @@ -3,11 +3,10 @@ import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.testcontainers.containers.JdbcDatabaseContainer; -import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; +import org.testcontainers.containers.wait.strategy.Wait; 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; @@ -68,11 +67,11 @@ public OracleContainer(String dockerImageName) { public OracleContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); - this.waitStrategy = - new LogMessageWaitStrategy() - .withRegEx(".*DATABASE IS READY TO USE!.*\\s") - .withTimes(1) - .withStartupTimeout(Duration.of(DEFAULT_STARTUP_TIMEOUT_SECONDS, ChronoUnit.SECONDS)); + 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); } @@ -90,7 +89,12 @@ public Set getLivenessCheckPortNumbers() { @Override public String getDriverClassName() { - return "oracle.jdbc.driver.OracleDriver"; + try { + Class.forName("oracle.jdbc.OracleDriver"); + return "oracle.jdbc.OracleDriver"; + } catch (ClassNotFoundException e) { + return "oracle.jdbc.driver.OracleDriver"; + } } @Override diff --git a/modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleR2DBCDatabaseContainer.java b/modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleR2DBCDatabaseContainer.java index f9c12955653..ae480027f25 100644 --- a/modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleR2DBCDatabaseContainer.java +++ b/modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleR2DBCDatabaseContainer.java @@ -1,17 +1,19 @@ package org.testcontainers.oracle; import io.r2dbc.spi.ConnectionFactoryOptions; -import lombok.RequiredArgsConstructor; -import lombok.experimental.Delegate; import org.testcontainers.lifecycle.Startable; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; -@RequiredArgsConstructor +import java.util.Set; + public class OracleR2DBCDatabaseContainer implements R2DBCDatabaseContainer { - @Delegate(types = Startable.class) private final OracleContainer container; + public OracleR2DBCDatabaseContainer(OracleContainer container) { + this.container = container; + } + public static ConnectionFactoryOptions getOptions(OracleContainer container) { ConnectionFactoryOptions options = ConnectionFactoryOptions .builder() @@ -32,4 +34,24 @@ public ConnectionFactoryOptions configure(ConnectionFactoryOptions options) { .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(); + } } diff --git a/modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleR2DBCDatabaseContainerProvider.java b/modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleR2DBCDatabaseContainerProvider.java index 6fe809aa214..d8fe1ee7084 100644 --- a/modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleR2DBCDatabaseContainerProvider.java +++ b/modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleR2DBCDatabaseContainerProvider.java @@ -1,13 +1,11 @@ package org.testcontainers.oracle; -import com.google.auto.service.AutoService; import io.r2dbc.spi.ConnectionFactoryMetadata; import io.r2dbc.spi.ConnectionFactoryOptions; import org.jetbrains.annotations.Nullable; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; import org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider; -@AutoService(R2DBCDatabaseContainerProvider.class) public class OracleR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider { static final String DRIVER = "oracle"; diff --git a/modules/oracle-free/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider b/modules/oracle-free/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider new file mode 100644 index 00000000000..20465a3e57b --- /dev/null +++ b/modules/oracle-free/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider @@ -0,0 +1 @@ +org.testcontainers.oracle.OracleR2DBCDatabaseContainerProvider diff --git a/modules/oracle-free/src/test/java/org/testcontainers/junit/oracle/SimpleOracleTest.java b/modules/oracle-free/src/test/java/org/testcontainers/junit/oracle/SimpleOracleTest.java index 5ef276c0157..f46539bac89 100644 --- a/modules/oracle-free/src/test/java/org/testcontainers/junit/oracle/SimpleOracleTest.java +++ b/modules/oracle-free/src/test/java/org/testcontainers/junit/oracle/SimpleOracleTest.java @@ -1,6 +1,6 @@ package org.testcontainers.junit.oracle; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.db.AbstractContainerDatabaseTest; import org.testcontainers.oracle.OracleContainer; import org.testcontainers.utility.DockerImageName; @@ -11,9 +11,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; -public class SimpleOracleTest extends AbstractContainerDatabaseTest { +class SimpleOracleTest extends AbstractContainerDatabaseTest { - public static final DockerImageName ORACLE_DOCKER_IMAGE_NAME = DockerImageName.parse( + private static final DockerImageName ORACLE_DOCKER_IMAGE_NAME = DockerImageName.parse( "gvenzl/oracle-free:slim-faststart" ); @@ -32,8 +32,11 @@ private void runTest(OracleContainer container, String databaseName, String user } @Test - public void testDefaultSettings() throws SQLException { - try (OracleContainer oracle = new OracleContainer(ORACLE_DOCKER_IMAGE_NAME);) { + void testDefaultSettings() throws SQLException { + try ( // container { + OracleContainer oracle = new OracleContainer("gvenzl/oracle-free:slim-faststart") + // } + ) { runTest(oracle, "freepdb1", "test", "test"); // Match against the last '/' @@ -43,28 +46,26 @@ public void testDefaultSettings() throws SQLException { } @Test - public void testPluggableDatabase() throws SQLException { + void testPluggableDatabase() throws SQLException { try (OracleContainer oracle = new OracleContainer(ORACLE_DOCKER_IMAGE_NAME).withDatabaseName("testDB")) { runTest(oracle, "testDB", "test", "test"); } } @Test - public void testPluggableDatabaseAndCustomUser() throws SQLException { + void testPluggableDatabaseAndCustomUser() throws SQLException { try ( - // constructor { OracleContainer oracle = new OracleContainer("gvenzl/oracle-free:slim-faststart") .withDatabaseName("testDB") .withUsername("testUser") .withPassword("testPassword") - // } ) { runTest(oracle, "testDB", "testUser", "testPassword"); } } @Test - public void testCustomUser() throws SQLException { + void testCustomUser() throws SQLException { try ( OracleContainer oracle = new OracleContainer(ORACLE_DOCKER_IMAGE_NAME) .withUsername("testUser") @@ -75,8 +76,8 @@ public void testCustomUser() throws SQLException { } @Test - public void testSID() throws SQLException { - try (OracleContainer oracle = new OracleContainer(ORACLE_DOCKER_IMAGE_NAME).usingSid();) { + void testSID() throws SQLException { + try (OracleContainer oracle = new OracleContainer(ORACLE_DOCKER_IMAGE_NAME).usingSid()) { runTest(oracle, "freepdb1", "system", "test"); // Match against the last ':' @@ -86,18 +87,18 @@ public void testSID() throws SQLException { } @Test - public void testSIDAndCustomPassword() throws SQLException { + void testSIDAndCustomPassword() throws SQLException { try ( OracleContainer oracle = new OracleContainer(ORACLE_DOCKER_IMAGE_NAME) .usingSid() - .withPassword("testPassword"); + .withPassword("testPassword") ) { runTest(oracle, "freepdb1", "system", "testPassword"); } } @Test - public void testErrorPaths() throws SQLException { + void testErrorPaths() throws SQLException { try (OracleContainer oracle = new OracleContainer(ORACLE_DOCKER_IMAGE_NAME)) { try { oracle.withDatabaseName("FREEPDB1"); diff --git a/modules/oracle-free/src/test/java/org/testcontainers/oracle/jdbc/OracleJDBCDriverTest.java b/modules/oracle-free/src/test/java/org/testcontainers/oracle/jdbc/OracleJDBCDriverTest.java index 6f6157c61c8..a4982e966c1 100644 --- a/modules/oracle-free/src/test/java/org/testcontainers/oracle/jdbc/OracleJDBCDriverTest.java +++ b/modules/oracle-free/src/test/java/org/testcontainers/oracle/jdbc/OracleJDBCDriverTest.java @@ -4,17 +4,17 @@ import com.zaxxer.hikari.HikariDataSource; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.ResultSetHandler; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.sql.ResultSet; import java.sql.SQLException; import static org.assertj.core.api.Assertions.assertThat; -public class OracleJDBCDriverTest { +class OracleJDBCDriverTest { @Test - public void testOracleWithNoSpecifiedVersion() throws SQLException { + void testOracleWithNoSpecifiedVersion() throws SQLException { performSimpleTest("jdbc:tc:oracle://hostname/databasename"); } diff --git a/modules/oracle-xe/build.gradle b/modules/oracle-xe/build.gradle index 3cef70efc4c..1fb43310ae0 100644 --- a/modules/oracle-xe/build.gradle +++ b/modules/oracle-xe/build.gradle @@ -1,32 +1,16 @@ description = "Testcontainers :: JDBC :: Oracle XE" dependencies { - annotationProcessor 'com.google.auto.service:auto-service:1.1.1' - compileOnly 'com.google.auto.service:auto-service:1.1.1' + api project(':testcontainers-jdbc') - api project(':jdbc') + compileOnly project(':testcontainers-r2dbc') + compileOnly 'com.oracle.database.r2dbc:oracle-r2dbc:1.3.0' - compileOnly project(':r2dbc') - compileOnly 'com.oracle.database.r2dbc:oracle-r2dbc:1.2.0' + testImplementation project(':testcontainers-jdbc-test') + testImplementation 'com.oracle.database.jdbc:ojdbc11:23.26.1.0.0' - testImplementation project(':jdbc-test') - testImplementation 'com.oracle.database.jdbc:ojdbc11:23.3.0.23.09' + compileOnly 'org.jetbrains:annotations:26.1.0' - compileOnly 'org.jetbrains:annotations:24.1.0' - - testImplementation testFixtures(project(':r2dbc')) - testRuntimeOnly 'com.oracle.database.r2dbc:oracle-r2dbc:1.2.0' -} - -test { - javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(11) - } -} - -compileTestJava { - javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(11) - } - options.release.set(11) + testImplementation testFixtures(project(':testcontainers-r2dbc')) + testRuntimeOnly 'com.oracle.database.r2dbc:oracle-r2dbc:1.3.0' } diff --git a/modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleContainer.java b/modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleContainer.java index 24dbdb37701..75b16779f40 100644 --- a/modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleContainer.java +++ b/modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleContainer.java @@ -110,7 +110,12 @@ public Set getLivenessCheckPortNumbers() { @Override public String getDriverClassName() { - return "oracle.jdbc.driver.OracleDriver"; + try { + Class.forName("oracle.jdbc.OracleDriver"); + return "oracle.jdbc.OracleDriver"; + } catch (ClassNotFoundException e) { + return "oracle.jdbc.driver.OracleDriver"; + } } @Override diff --git a/modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleR2DBCDatabaseContainerProvider.java b/modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleR2DBCDatabaseContainerProvider.java index 4d6a03530c2..35a98fda77d 100644 --- a/modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleR2DBCDatabaseContainerProvider.java +++ b/modules/oracle-xe/src/main/java/org/testcontainers/containers/OracleR2DBCDatabaseContainerProvider.java @@ -1,13 +1,11 @@ package org.testcontainers.containers; -import com.google.auto.service.AutoService; import io.r2dbc.spi.ConnectionFactoryMetadata; import io.r2dbc.spi.ConnectionFactoryOptions; import org.jetbrains.annotations.Nullable; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; import org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider; -@AutoService(R2DBCDatabaseContainerProvider.class) public class OracleR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider { static final String DRIVER = "oracle"; diff --git a/modules/oracle-xe/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider b/modules/oracle-xe/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider new file mode 100644 index 00000000000..cd1df0b4488 --- /dev/null +++ b/modules/oracle-xe/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider @@ -0,0 +1 @@ +org.testcontainers.containers.OracleR2DBCDatabaseContainerProvider diff --git a/modules/oracle-xe/src/test/java/org/testcontainers/containers/jdbc/OracleJDBCDriverTest.java b/modules/oracle-xe/src/test/java/org/testcontainers/containers/jdbc/OracleJDBCDriverTest.java index 5e44f0d368c..4b672827fe6 100644 --- a/modules/oracle-xe/src/test/java/org/testcontainers/containers/jdbc/OracleJDBCDriverTest.java +++ b/modules/oracle-xe/src/test/java/org/testcontainers/containers/jdbc/OracleJDBCDriverTest.java @@ -4,17 +4,17 @@ import com.zaxxer.hikari.HikariDataSource; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.ResultSetHandler; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.sql.ResultSet; import java.sql.SQLException; import static org.assertj.core.api.Assertions.assertThat; -public class OracleJDBCDriverTest { +class OracleJDBCDriverTest { @Test - public void testOracleWithNoSpecifiedVersion() throws SQLException { + void testOracleWithNoSpecifiedVersion() throws SQLException { performSimpleTest("jdbc:tc:oracle://hostname/databasename"); } diff --git a/modules/oracle-xe/src/test/java/org/testcontainers/junit/oracle/SimpleOracleTest.java b/modules/oracle-xe/src/test/java/org/testcontainers/junit/oracle/SimpleOracleTest.java index 82ef9846ab2..17ee85944a7 100644 --- a/modules/oracle-xe/src/test/java/org/testcontainers/junit/oracle/SimpleOracleTest.java +++ b/modules/oracle-xe/src/test/java/org/testcontainers/junit/oracle/SimpleOracleTest.java @@ -1,6 +1,6 @@ package org.testcontainers.junit.oracle; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.OracleContainer; import org.testcontainers.db.AbstractContainerDatabaseTest; import org.testcontainers.utility.DockerImageName; @@ -11,7 +11,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; -public class SimpleOracleTest extends AbstractContainerDatabaseTest { +class SimpleOracleTest extends AbstractContainerDatabaseTest { public static final DockerImageName ORACLE_DOCKER_IMAGE_NAME = DockerImageName.parse( "gvenzl/oracle-xe:21-slim-faststart" @@ -32,8 +32,11 @@ private void runTest(OracleContainer container, String databaseName, String user } @Test - public void testDefaultSettings() throws SQLException { - try (OracleContainer oracle = new OracleContainer(ORACLE_DOCKER_IMAGE_NAME);) { + 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 '/' @@ -43,28 +46,26 @@ public void testDefaultSettings() throws SQLException { } @Test - public void testPluggableDatabase() throws SQLException { + void testPluggableDatabase() throws SQLException { try (OracleContainer oracle = new OracleContainer(ORACLE_DOCKER_IMAGE_NAME).withDatabaseName("testDB")) { runTest(oracle, "testDB", "test", "test"); } } @Test - public void testPluggableDatabaseAndCustomUser() throws SQLException { + void testPluggableDatabaseAndCustomUser() throws SQLException { try ( - // constructor { OracleContainer oracle = new OracleContainer("gvenzl/oracle-xe:21-slim-faststart") .withDatabaseName("testDB") .withUsername("testUser") .withPassword("testPassword") - // } ) { runTest(oracle, "testDB", "testUser", "testPassword"); } } @Test - public void testCustomUser() throws SQLException { + void testCustomUser() throws SQLException { try ( OracleContainer oracle = new OracleContainer(ORACLE_DOCKER_IMAGE_NAME) .withUsername("testUser") @@ -75,7 +76,7 @@ public void testCustomUser() throws SQLException { } @Test - public void testSID() throws SQLException { + void testSID() throws SQLException { try (OracleContainer oracle = new OracleContainer(ORACLE_DOCKER_IMAGE_NAME).usingSid();) { runTest(oracle, "xepdb1", "system", "test"); @@ -86,7 +87,7 @@ public void testSID() throws SQLException { } @Test - public void testSIDAndCustomPassword() throws SQLException { + void testSIDAndCustomPassword() throws SQLException { try ( OracleContainer oracle = new OracleContainer(ORACLE_DOCKER_IMAGE_NAME) .usingSid() @@ -97,7 +98,7 @@ public void testSIDAndCustomPassword() throws SQLException { } @Test - public void testErrorPaths() throws SQLException { + void testErrorPaths() throws SQLException { try (OracleContainer oracle = new OracleContainer(ORACLE_DOCKER_IMAGE_NAME)) { try { oracle.withDatabaseName("XEPDB1"); diff --git a/modules/orientdb/build.gradle b/modules/orientdb/build.gradle index 09e47b14ce7..6d691b407ce 100644 --- a/modules/orientdb/build.gradle +++ b/modules/orientdb/build.gradle @@ -3,9 +3,8 @@ description = "Testcontainers :: Orientdb" dependencies { api project(":testcontainers") - api "com.orientechnologies:orientdb-client:3.2.26" + api "com.orientechnologies:orientdb-client:3.2.53" - testImplementation 'org.assertj:assertj-core:3.25.1' - testImplementation 'org.apache.tinkerpop:gremlin-driver:3.7.1' - testImplementation "com.orientechnologies:orientdb-gremlin:3.2.26" + testImplementation 'org.apache.tinkerpop:gremlin-driver:3.8.1' + testImplementation "com.orientechnologies:orientdb-gremlin:3.2.53" } diff --git a/modules/orientdb/src/main/java/org/testcontainers/containers/OrientDBContainer.java b/modules/orientdb/src/main/java/org/testcontainers/containers/OrientDBContainer.java index 12a6e58e4a5..e21472dd16a 100644 --- a/modules/orientdb/src/main/java/org/testcontainers/containers/OrientDBContainer.java +++ b/modules/orientdb/src/main/java/org/testcontainers/containers/OrientDBContainer.java @@ -9,7 +9,7 @@ import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; +import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.ComparableVersion; import org.testcontainers.utility.DockerImageName; @@ -29,7 +29,10 @@ *
  • 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); @@ -79,7 +82,7 @@ public OrientDBContainer(final DockerImageName dockerImageName) { serverPassword = DEFAULT_SERVER_PASSWORD; databaseName = DEFAULT_DATABASE_NAME; - waitStrategy = new LogMessageWaitStrategy().withRegEx(".*OrientDB Studio available.*"); + waitStrategy = Wait.forLogMessage(".*OrientDB Studio available.*", 1); addExposedPorts(DEFAULT_BINARY_PORT, DEFAULT_HTTP_PORT); } diff --git a/modules/orientdb/src/main/java/org/testcontainers/orientdb/OrientDBContainer.java b/modules/orientdb/src/main/java/org/testcontainers/orientdb/OrientDBContainer.java new file mode 100644 index 00000000000..753d32951e7 --- /dev/null +++ b/modules/orientdb/src/main/java/org/testcontainers/orientdb/OrientDBContainer.java @@ -0,0 +1,140 @@ +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; + } +} diff --git a/modules/orientdb/src/test/java/org/testcontainers/containers/OrientDBContainerTest.java b/modules/orientdb/src/test/java/org/testcontainers/containers/OrientDBContainerTest.java deleted file mode 100644 index d370dc71472..00000000000 --- a/modules/orientdb/src/test/java/org/testcontainers/containers/OrientDBContainerTest.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.testcontainers.containers; - -import com.orientechnologies.orient.core.db.ODatabaseSession; -import org.junit.Test; -import org.testcontainers.utility.DockerImageName; -import org.testcontainers.utility.MountableFile; - -import static org.assertj.core.api.Assertions.assertThat; - -public class OrientDBContainerTest { - - private static final DockerImageName ORIENTDB_IMAGE = DockerImageName.parse("orientdb:3.2.0-tp3"); - - @Test - public void shouldReturnTheSameSession() { - try (OrientDBContainer container = new OrientDBContainer(ORIENTDB_IMAGE)) { - container.start(); - - final ODatabaseSession session = container.getSession(); - final ODatabaseSession session2 = container.getSession(); - - assertThat(session).isSameAs(session2); - } - } - - @Test - public void shouldInitializeWithCommands() { - try (OrientDBContainer container = new OrientDBContainer(ORIENTDB_IMAGE)) { - container.start(); - - final ODatabaseSession session = container.getSession(); - - 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 - public void shouldQueryWithGremlin() { - try ( - OrientDBContainer container = new OrientDBContainer(ORIENTDB_IMAGE) - .withCopyFileToContainer( - MountableFile.forClasspathResource("orientdb-server-config.xml"), - "/orientdb/config/orientdb-server-config.xml" - ) - ) { - container.start(); - - final ODatabaseSession session = container.getSession("admin", "admin"); - - 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 - public void shouldInitializeDatabaseFromScript() { - try ( - OrientDBContainer container = new OrientDBContainer(ORIENTDB_IMAGE) - .withScriptPath("initscript.osql") - .withDatabaseName("persons") - ) { - container.start(); - - assertThat(container.getDbUrl()) - .isEqualTo("remote:" + container.getHost() + ":" + container.getMappedPort(2424) + "/persons"); - - final ODatabaseSession session = container.getSession(); - - assertThat(session.query("SELECT FROM Person").stream()).hasSize(4); - } - } -} diff --git a/modules/orientdb/src/test/java/org/testcontainers/orientdb/OrientDBContainerTest.java b/modules/orientdb/src/test/java/org/testcontainers/orientdb/OrientDBContainerTest.java new file mode 100644 index 00000000000..2952b70f216 --- /dev/null +++ b/modules/orientdb/src/test/java/org/testcontainers/orientdb/OrientDBContainerTest.java @@ -0,0 +1,102 @@ +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); + } + } +} diff --git a/modules/pinecone/build.gradle b/modules/pinecone/build.gradle new file mode 100644 index 00000000000..ad46d3ce9d9 --- /dev/null +++ b/modules/pinecone/build.gradle @@ -0,0 +1,7 @@ +description = "Testcontainers :: Pinecone" + +dependencies { + api project(':testcontainers') + + testImplementation 'io.pinecone:pinecone-client:3.1.0' +} diff --git a/modules/pinecone/src/main/java/org/testcontainers/pinecone/PineconeLocalContainer.java b/modules/pinecone/src/main/java/org/testcontainers/pinecone/PineconeLocalContainer.java new file mode 100644 index 00000000000..56c846872de --- /dev/null +++ b/modules/pinecone/src/main/java/org/testcontainers/pinecone/PineconeLocalContainer.java @@ -0,0 +1,34 @@ +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); + } +} diff --git a/modules/pinecone/src/test/java/org/testcontainers/pinecone/PineconeLocalContainerTest.java b/modules/pinecone/src/test/java/org/testcontainers/pinecone/PineconeLocalContainerTest.java new file mode 100644 index 00000000000..6303e069423 --- /dev/null +++ b/modules/pinecone/src/test/java/org/testcontainers/pinecone/PineconeLocalContainerTest.java @@ -0,0 +1,33 @@ +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); + } + } +} diff --git a/modules/pinecone/src/test/resources/logback-test.xml b/modules/pinecone/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..83ef7a1a3ef --- /dev/null +++ b/modules/pinecone/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + diff --git a/modules/postgresql/build.gradle b/modules/postgresql/build.gradle index 0177c8c6b0e..db7382f34d3 100644 --- a/modules/postgresql/build.gradle +++ b/modules/postgresql/build.gradle @@ -1,19 +1,16 @@ description = "Testcontainers :: JDBC :: PostgreSQL" dependencies { - annotationProcessor 'com.google.auto.service:auto-service:1.1.1' - compileOnly 'com.google.auto.service:auto-service:1.1.1' + api project(':testcontainers-jdbc') - api project(':jdbc') - - compileOnly project(':r2dbc') + compileOnly project(':testcontainers-r2dbc') compileOnly 'io.r2dbc:r2dbc-postgresql:0.8.13.RELEASE' - testImplementation project(':jdbc-test') - testRuntimeOnly 'org.postgresql:postgresql:42.7.1' + testImplementation project(':testcontainers-jdbc-test') + testRuntimeOnly 'org.postgresql:postgresql:42.7.12' - testImplementation testFixtures(project(':r2dbc')) + testImplementation testFixtures(project(':testcontainers-r2dbc')) testRuntimeOnly 'io.r2dbc:r2dbc-postgresql:0.8.13.RELEASE' - compileOnly 'org.jetbrains:annotations:24.1.0' + compileOnly 'org.jetbrains:annotations:26.1.0' } diff --git a/modules/postgresql/src/main/java/org/testcontainers/containers/PgVectorContainerProvider.java b/modules/postgresql/src/main/java/org/testcontainers/containers/PgVectorContainerProvider.java new file mode 100644 index 00000000000..29f6c3bf935 --- /dev/null +++ b/modules/postgresql/src/main/java/org/testcontainers/containers/PgVectorContainerProvider.java @@ -0,0 +1,42 @@ +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); + } +} diff --git a/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLContainer.java b/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLContainer.java index bc276f4cc1b..4824654957f 100644 --- a/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLContainer.java +++ b/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLContainer.java @@ -11,10 +11,13 @@ /** * Testcontainers implementation for PostgreSQL. *

    - * Supported image: {@code postgres} + * 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"; @@ -25,6 +28,8 @@ public class PostgreSQLContainer> extends 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"; @@ -53,7 +58,7 @@ public PostgreSQLContainer(final String dockerImageName) { public PostgreSQLContainer(final DockerImageName dockerImageName) { super(dockerImageName); - dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, PGVECTOR_IMAGE_NAME); this.waitStrategy = new LogMessageWaitStrategy() diff --git a/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerProvider.java b/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerProvider.java index 6b4a81c5e40..f1bdf67ee5c 100644 --- a/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerProvider.java +++ b/modules/postgresql/src/main/java/org/testcontainers/containers/PostgreSQLR2DBCDatabaseContainerProvider.java @@ -1,6 +1,5 @@ package org.testcontainers.containers; -import com.google.auto.service.AutoService; import io.r2dbc.postgresql.PostgresqlConnectionFactoryProvider; import io.r2dbc.spi.ConnectionFactoryMetadata; import io.r2dbc.spi.ConnectionFactoryOptions; @@ -9,7 +8,6 @@ import javax.annotation.Nullable; -@AutoService(R2DBCDatabaseContainerProvider.class) public final class PostgreSQLR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider { static final String DRIVER = PostgresqlConnectionFactoryProvider.POSTGRESQL_DRIVER; diff --git a/modules/postgresql/src/main/java/org/testcontainers/postgresql/PostgreSQLContainer.java b/modules/postgresql/src/main/java/org/testcontainers/postgresql/PostgreSQLContainer.java new file mode 100644 index 00000000000..27521487a23 --- /dev/null +++ b/modules/postgresql/src/main/java/org/testcontainers/postgresql/PostgreSQLContainer.java @@ -0,0 +1,144 @@ +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); + } +} diff --git a/modules/postgresql/src/main/java/org/testcontainers/postgresql/PostgreSQLR2DBCDatabaseContainer.java b/modules/postgresql/src/main/java/org/testcontainers/postgresql/PostgreSQLR2DBCDatabaseContainer.java new file mode 100644 index 00000000000..d99d638b100 --- /dev/null +++ b/modules/postgresql/src/main/java/org/testcontainers/postgresql/PostgreSQLR2DBCDatabaseContainer.java @@ -0,0 +1,58 @@ +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(); + } +} diff --git a/modules/postgresql/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider b/modules/postgresql/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider index 05df9054522..33429fa8b1e 100644 --- a/modules/postgresql/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider +++ b/modules/postgresql/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider @@ -1,3 +1,4 @@ org.testcontainers.containers.PostgreSQLContainerProvider org.testcontainers.containers.PostgisContainerProvider org.testcontainers.containers.TimescaleDBContainerProvider +org.testcontainers.containers.PgVectorContainerProvider diff --git a/modules/postgresql/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider b/modules/postgresql/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider new file mode 100644 index 00000000000..6224c0e6093 --- /dev/null +++ b/modules/postgresql/src/main/resources/META-INF/services/org.testcontainers.r2dbc.R2DBCDatabaseContainerProvider @@ -0,0 +1 @@ +org.testcontainers.containers.PostgreSQLR2DBCDatabaseContainerProvider diff --git a/modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLConnectionURLTest.java b/modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLConnectionURLTest.java index 275517250b6..3ed0b7030b1 100644 --- a/modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLConnectionURLTest.java +++ b/modules/postgresql/src/test/java/org/testcontainers/containers/PostgreSQLConnectionURLTest.java @@ -1,15 +1,15 @@ package org.testcontainers.containers; -import org.junit.Test; +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; -public class PostgreSQLConnectionURLTest { +class PostgreSQLConnectionURLTest { @Test - public void shouldCorrectlyAppendQueryString() { + void shouldCorrectlyAppendQueryString() { PostgreSQLContainer postgres = new FixedJdbcUrlPostgreSQLContainer(); String connectionUrl = postgres.constructUrlForConnection("?stringtype=unspecified&stringtype=unspecified"); String queryString = connectionUrl.substring(connectionUrl.indexOf('?')); @@ -22,7 +22,7 @@ public void shouldCorrectlyAppendQueryString() { } @Test - public void shouldCorrectlyAppendQueryStringWhenNoBaseParams() { + void shouldCorrectlyAppendQueryStringWhenNoBaseParams() { PostgreSQLContainer postgres = new NoParamsUrlPostgreSQLContainer(); String connectionUrl = postgres.constructUrlForConnection("?stringtype=unspecified&stringtype=unspecified"); String queryString = connectionUrl.substring(connectionUrl.indexOf('?')); @@ -35,7 +35,7 @@ public void shouldCorrectlyAppendQueryStringWhenNoBaseParams() { } @Test - public void shouldReturnOriginalURLWhenEmptyQueryString() { + void shouldReturnOriginalURLWhenEmptyQueryString() { PostgreSQLContainer postgres = new FixedJdbcUrlPostgreSQLContainer(); String connectionUrl = postgres.constructUrlForConnection(""); @@ -43,7 +43,7 @@ public void shouldReturnOriginalURLWhenEmptyQueryString() { } @Test - public void shouldRejectInvalidQueryString() { + void shouldRejectInvalidQueryString() { assertThat( catchThrowable(() -> { new NoParamsUrlPostgreSQLContainer().constructUrlForConnection("stringtype=unspecified"); diff --git a/modules/postgresql/src/test/java/org/testcontainers/containers/TimescaleDBContainerTest.java b/modules/postgresql/src/test/java/org/testcontainers/containers/TimescaleDBContainerTest.java index f49c7dc6aa7..3991f4c2b1c 100644 --- a/modules/postgresql/src/test/java/org/testcontainers/containers/TimescaleDBContainerTest.java +++ b/modules/postgresql/src/test/java/org/testcontainers/containers/TimescaleDBContainerTest.java @@ -1,6 +1,6 @@ package org.testcontainers.containers; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.db.AbstractContainerDatabaseTest; import java.sql.ResultSet; @@ -8,10 +8,10 @@ import static org.assertj.core.api.Assertions.assertThat; -public class TimescaleDBContainerTest extends AbstractContainerDatabaseTest { +class TimescaleDBContainerTest extends AbstractContainerDatabaseTest { @Test - public void testSimple() throws SQLException { + void testSimple() throws SQLException { try (JdbcDatabaseContainer postgres = new TimescaleDBContainerProvider().newInstance()) { postgres.start(); @@ -22,7 +22,7 @@ public void testSimple() throws SQLException { } @Test - public void testCommandOverride() throws SQLException { + void testCommandOverride() throws SQLException { try ( GenericContainer postgres = new TimescaleDBContainerProvider() .newInstance() @@ -35,12 +35,12 @@ public void testCommandOverride() throws SQLException { "SELECT current_setting('max_connections')" ); String result = resultSet.getString(1); - assertThat(result).as("max_connections should be overriden").isEqualTo("42"); + assertThat(result).as("max_connections should be overridden").isEqualTo("42"); } } @Test - public void testUnsetCommand() throws SQLException { + void testUnsetCommand() throws SQLException { try ( GenericContainer postgres = new TimescaleDBContainerProvider() .newInstance() @@ -54,12 +54,12 @@ public void testUnsetCommand() throws SQLException { "SELECT current_setting('max_connections')" ); String result = resultSet.getString(1); - assertThat(result).as("max_connections should not be overriden").isNotEqualTo("42"); + assertThat(result).as("max_connections should not be overridden").isNotEqualTo("42"); } } @Test - public void testExplicitInitScript() throws SQLException { + void testExplicitInitScript() throws SQLException { try ( JdbcDatabaseContainer postgres = new TimescaleDBContainerProvider() .newInstance() diff --git a/modules/postgresql/src/test/java/org/testcontainers/jdbc/DatabaseDriverShutdownTest.java b/modules/postgresql/src/test/java/org/testcontainers/jdbc/DatabaseDriverShutdownTest.java index ed519ed2e4b..ab4d6f03d2e 100644 --- a/modules/postgresql/src/test/java/org/testcontainers/jdbc/DatabaseDriverShutdownTest.java +++ b/modules/postgresql/src/test/java/org/testcontainers/jdbc/DatabaseDriverShutdownTest.java @@ -1,7 +1,7 @@ package org.testcontainers.jdbc; -import org.junit.AfterClass; -import org.junit.Test; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.JdbcDatabaseContainer; import java.sql.Connection; @@ -16,15 +16,15 @@ * 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. */ -public class DatabaseDriverShutdownTest { +class DatabaseDriverShutdownTest { - @AfterClass + @BeforeAll public static void testCleanup() { ContainerDatabaseDriver.killContainers(); } @Test - public void shouldStopContainerWhenAllConnectionsClosed() throws SQLException { + void shouldStopContainerWhenAllConnectionsClosed() throws SQLException { final String jdbcUrl = "jdbc:tc:postgresql:9.6.8://hostname/databasename"; getConnectionAndClose(jdbcUrl); @@ -34,7 +34,7 @@ public void shouldStopContainerWhenAllConnectionsClosed() throws SQLException { } @Test - public void shouldNotStopDaemonContainerWhenAllConnectionsClosed() throws SQLException { + void shouldNotStopDaemonContainerWhenAllConnectionsClosed() throws SQLException { final String jdbcUrl = "jdbc:tc:postgresql:9.6.8://hostname/databasename?TC_DAEMON=true"; getConnectionAndClose(jdbcUrl); diff --git a/modules/postgresql/src/test/java/org/testcontainers/jdbc/DatabaseDriverTmpfsTest.java b/modules/postgresql/src/test/java/org/testcontainers/jdbc/DatabaseDriverTmpfsTest.java index 463981d2d59..12a6f2216cf 100644 --- a/modules/postgresql/src/test/java/org/testcontainers/jdbc/DatabaseDriverTmpfsTest.java +++ b/modules/postgresql/src/test/java/org/testcontainers/jdbc/DatabaseDriverTmpfsTest.java @@ -1,6 +1,6 @@ package org.testcontainers.jdbc; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.Container; import org.testcontainers.containers.JdbcDatabaseContainer; @@ -15,10 +15,10 @@ * 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. */ -public class DatabaseDriverTmpfsTest { +class DatabaseDriverTmpfsTest { @Test - public void testDatabaseHasTmpFsViaConnectionString() throws Exception { + 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); diff --git a/modules/postgresql/src/test/java/org/testcontainers/jdbc/pgvector/PgVectorJDBCDriverTest.java b/modules/postgresql/src/test/java/org/testcontainers/jdbc/pgvector/PgVectorJDBCDriverTest.java new file mode 100644 index 00000000000..e94986e7875 --- /dev/null +++ b/modules/postgresql/src/test/java/org/testcontainers/jdbc/pgvector/PgVectorJDBCDriverTest.java @@ -0,0 +1,24 @@ +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), + }, + } + ); + } +} diff --git a/modules/postgresql/src/test/java/org/testcontainers/jdbc/postgis/PostgisJDBCDriverTest.java b/modules/postgresql/src/test/java/org/testcontainers/jdbc/postgis/PostgisJDBCDriverTest.java index abc0a3b76bc..d6a0047a23c 100644 --- a/modules/postgresql/src/test/java/org/testcontainers/jdbc/postgis/PostgisJDBCDriverTest.java +++ b/modules/postgresql/src/test/java/org/testcontainers/jdbc/postgis/PostgisJDBCDriverTest.java @@ -1,16 +1,12 @@ package org.testcontainers.jdbc.postgis; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; -@RunWith(Parameterized.class) -public class PostgisJDBCDriverTest extends AbstractJDBCDriverTest { +class PostgisJDBCDriverTest extends AbstractJDBCDriverTest { - @Parameterized.Parameters(name = "{index} - {0}") public static Iterable data() { return Arrays.asList( new Object[][] { diff --git a/modules/postgresql/src/test/java/org/testcontainers/jdbc/postgresql/PostgreSQLJDBCDriverTest.java b/modules/postgresql/src/test/java/org/testcontainers/jdbc/postgresql/PostgreSQLJDBCDriverTest.java index d42f17f3ec2..b47af39f54b 100644 --- a/modules/postgresql/src/test/java/org/testcontainers/jdbc/postgresql/PostgreSQLJDBCDriverTest.java +++ b/modules/postgresql/src/test/java/org/testcontainers/jdbc/postgresql/PostgreSQLJDBCDriverTest.java @@ -1,16 +1,12 @@ package org.testcontainers.jdbc.postgresql; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; -@RunWith(Parameterized.class) public class PostgreSQLJDBCDriverTest extends AbstractJDBCDriverTest { - @Parameterized.Parameters(name = "{index} - {0}") public static Iterable data() { return Arrays.asList( new Object[][] { diff --git a/modules/postgresql/src/test/java/org/testcontainers/jdbc/timescaledb/TimescaleDBJDBCDriverTest.java b/modules/postgresql/src/test/java/org/testcontainers/jdbc/timescaledb/TimescaleDBJDBCDriverTest.java index 2a50b46c901..8befb01bfb1 100644 --- a/modules/postgresql/src/test/java/org/testcontainers/jdbc/timescaledb/TimescaleDBJDBCDriverTest.java +++ b/modules/postgresql/src/test/java/org/testcontainers/jdbc/timescaledb/TimescaleDBJDBCDriverTest.java @@ -1,16 +1,12 @@ package org.testcontainers.jdbc.timescaledb; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; -@RunWith(Parameterized.class) public class TimescaleDBJDBCDriverTest extends AbstractJDBCDriverTest { - @Parameterized.Parameters(name = "{index} - {0}") public static Iterable data() { return Arrays.asList( new Object[][] { diff --git a/modules/postgresql/src/test/java/org/testcontainers/junit/postgresql/CustomizablePostgreSQLTest.java b/modules/postgresql/src/test/java/org/testcontainers/junit/postgresql/CustomizablePostgreSQLTest.java deleted file mode 100644 index 0e9904a5f61..00000000000 --- a/modules/postgresql/src/test/java/org/testcontainers/junit/postgresql/CustomizablePostgreSQLTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.testcontainers.junit.postgresql; - -import org.junit.Test; -import org.testcontainers.PostgreSQLTestImages; -import org.testcontainers.containers.PostgreSQLContainer; -import org.testcontainers.db.AbstractContainerDatabaseTest; - -import java.sql.ResultSet; -import java.sql.SQLException; - -import static org.assertj.core.api.Assertions.assertThat; - -public class CustomizablePostgreSQLTest extends AbstractContainerDatabaseTest { - - private static final String DB_NAME = "foo"; - - private static final String USER = "bar"; - - private static final String PWD = "baz"; - - @Test - public void testSimple() throws SQLException { - try ( - PostgreSQLContainer postgres = new PostgreSQLContainer<>(PostgreSQLTestImages.POSTGRES_TEST_IMAGE) - .withDatabaseName(DB_NAME) - .withUsername(USER) - .withPassword(PWD) - ) { - postgres.start(); - - ResultSet resultSet = performQuery(postgres, "SELECT 1"); - - int resultSetInt = resultSet.getInt(1); - assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); - } - } -} diff --git a/modules/postgresql/src/test/java/org/testcontainers/postgresql/CompatibleImageTest.java b/modules/postgresql/src/test/java/org/testcontainers/postgresql/CompatibleImageTest.java new file mode 100644 index 00000000000..bdad4fb6a5c --- /dev/null +++ b/modules/postgresql/src/test/java/org/testcontainers/postgresql/CompatibleImageTest.java @@ -0,0 +1,62 @@ +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); + } + } +} diff --git a/modules/postgresql/src/test/java/org/testcontainers/junit/postgresql/SimplePostgreSQLTest.java b/modules/postgresql/src/test/java/org/testcontainers/postgresql/PostgreSQLContainerTest.java similarity index 53% rename from modules/postgresql/src/test/java/org/testcontainers/junit/postgresql/SimplePostgreSQLTest.java rename to modules/postgresql/src/test/java/org/testcontainers/postgresql/PostgreSQLContainerTest.java index 84beb779fa4..beefa3776b1 100644 --- a/modules/postgresql/src/test/java/org/testcontainers/junit/postgresql/SimplePostgreSQLTest.java +++ b/modules/postgresql/src/test/java/org/testcontainers/postgresql/PostgreSQLContainerTest.java @@ -1,8 +1,7 @@ -package org.testcontainers.junit.postgresql; +package org.testcontainers.postgresql; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.PostgreSQLTestImages; -import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.db.AbstractContainerDatabaseTest; import java.sql.ResultSet; @@ -11,16 +10,20 @@ import java.util.logging.LogManager; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; -public class SimplePostgreSQLTest extends AbstractContainerDatabaseTest { +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 - public void testSimple() throws SQLException { - try (PostgreSQLContainer postgres = new PostgreSQLContainer<>(PostgreSQLTestImages.POSTGRES_TEST_IMAGE)) { + void testSimple() throws SQLException { + try ( // container { + PostgreSQLContainer postgres = new PostgreSQLContainer("postgres:9.6.12") + // } + ) { postgres.start(); ResultSet resultSet = performQuery(postgres, "SELECT 1"); @@ -31,23 +34,23 @@ public void testSimple() throws SQLException { } @Test - public void testCommandOverride() throws SQLException { + void testCommandOverride() throws SQLException { try ( - PostgreSQLContainer postgres = new PostgreSQLContainer<>(PostgreSQLTestImages.POSTGRES_TEST_IMAGE) + 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 overriden").isEqualTo("42"); + assertThat(result).as("max_connections should be overridden").isEqualTo("42"); } } @Test - public void testUnsetCommand() throws SQLException { + void testUnsetCommand() throws SQLException { try ( - PostgreSQLContainer postgres = new PostgreSQLContainer<>(PostgreSQLTestImages.POSTGRES_TEST_IMAGE) + PostgreSQLContainer postgres = new PostgreSQLContainer(PostgreSQLTestImages.POSTGRES_TEST_IMAGE) .withCommand("postgres -c max_connections=42") .withCommand() ) { @@ -55,14 +58,24 @@ public void testUnsetCommand() throws SQLException { ResultSet resultSet = performQuery(postgres, "SELECT current_setting('max_connections')"); String result = resultSet.getString(1); - assertThat(result).as("max_connections should not be overriden").isNotEqualTo("42"); + 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 - public void testExplicitInitScript() throws SQLException { + void testExplicitInitScript() throws SQLException { try ( - PostgreSQLContainer postgres = new PostgreSQLContainer<>(PostgreSQLTestImages.POSTGRES_TEST_IMAGE) + PostgreSQLContainer postgres = new PostgreSQLContainer(PostgreSQLTestImages.POSTGRES_TEST_IMAGE) .withInitScript("somepath/init_postgresql.sql") ) { postgres.start(); @@ -75,9 +88,30 @@ public void testExplicitInitScript() throws SQLException { } @Test - public void testWithAdditionalUrlParamInJdbcUrl() { + 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) + PostgreSQLContainer postgres = new PostgreSQLContainer(PostgreSQLTestImages.POSTGRES_TEST_IMAGE) .withUrlParam("charSet", "UNICODE") ) { postgres.start(); @@ -88,7 +122,7 @@ public void testWithAdditionalUrlParamInJdbcUrl() { } } - private void assertHasCorrectExposedAndLivenessCheckPorts(PostgreSQLContainer postgres) { + private void assertHasCorrectExposedAndLivenessCheckPorts(PostgreSQLContainer postgres) { assertThat(postgres.getExposedPorts()).containsExactly(PostgreSQLContainer.POSTGRESQL_PORT); assertThat(postgres.getLivenessCheckPortNumbers()) .containsExactly(postgres.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT)); diff --git a/modules/postgresql/src/test/java/org/testcontainers/postgresql/PostgreSQLR2DBCDatabaseContainerTest.java b/modules/postgresql/src/test/java/org/testcontainers/postgresql/PostgreSQLR2DBCDatabaseContainerTest.java new file mode 100644 index 00000000000..4f5386d9f58 --- /dev/null +++ b/modules/postgresql/src/test/java/org/testcontainers/postgresql/PostgreSQLR2DBCDatabaseContainerTest.java @@ -0,0 +1,30 @@ +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"; + } +} diff --git a/modules/postgresql/src/test/resources/somepath/init_postgresql_2.sql b/modules/postgresql/src/test/resources/somepath/init_postgresql_2.sql new file mode 100644 index 00000000000..f4ecf9bbfad --- /dev/null +++ b/modules/postgresql/src/test/resources/somepath/init_postgresql_2.sql @@ -0,0 +1,5 @@ +CREATE TABLE foo ( + bar VARCHAR(255) +); + +INSERT INTO foo (bar) VALUES ('hello world 2'); diff --git a/modules/presto/build.gradle b/modules/presto/build.gradle index ad35cd6ec25..1d2933dec53 100644 --- a/modules/presto/build.gradle +++ b/modules/presto/build.gradle @@ -1,9 +1,9 @@ description = "Testcontainers :: JDBC :: Presto" dependencies { - api project(':jdbc') + api project(':testcontainers-jdbc') - testImplementation project(':jdbc-test') + testImplementation project(':testcontainers-jdbc-test') testRuntimeOnly 'io.prestosql:presto-jdbc:350' - compileOnly 'org.jetbrains:annotations:24.1.0' + compileOnly 'org.jetbrains:annotations:26.1.0' } diff --git a/modules/presto/src/main/java/org/testcontainers/containers/PrestoContainer.java b/modules/presto/src/main/java/org/testcontainers/containers/PrestoContainer.java index 946f6a95c6d..ac01eff1882 100644 --- a/modules/presto/src/main/java/org/testcontainers/containers/PrestoContainer.java +++ b/modules/presto/src/main/java/org/testcontainers/containers/PrestoContainer.java @@ -2,7 +2,7 @@ import com.google.common.base.Strings; import org.jetbrains.annotations.NotNull; -import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; +import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import java.sql.Connection; @@ -47,10 +47,11 @@ public PrestoContainer(final DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); - this.waitStrategy = - new LogMessageWaitStrategy() - .withRegEx(".*======== SERVER STARTED ========.*") - .withStartupTimeout(Duration.of(60, ChronoUnit.SECONDS)); + waitingFor( + Wait + .forLogMessage(".*======== SERVER STARTED ========.*", 1) + .withStartupTimeout(Duration.of(60, ChronoUnit.SECONDS)) + ); addExposedPort(PRESTO_PORT); } diff --git a/modules/presto/src/test/java/org/testcontainers/PrestoTestImages.java b/modules/presto/src/test/java/org/testcontainers/PrestoTestImages.java index 174cb7b4d4a..498e09f0be4 100644 --- a/modules/presto/src/test/java/org/testcontainers/PrestoTestImages.java +++ b/modules/presto/src/test/java/org/testcontainers/PrestoTestImages.java @@ -4,5 +4,6 @@ public interface PrestoTestImages { DockerImageName PRESTO_TEST_IMAGE = DockerImageName.parse("ghcr.io/trinodb/presto:344"); + DockerImageName PRESTO_PREVIOUS_VERSION_TEST_IMAGE = DockerImageName.parse("ghcr.io/trinodb/presto:343"); } diff --git a/modules/presto/src/test/java/org/testcontainers/containers/PrestoContainerTest.java b/modules/presto/src/test/java/org/testcontainers/containers/PrestoContainerTest.java index 8f2aa15f21c..9a46309f9e9 100644 --- a/modules/presto/src/test/java/org/testcontainers/containers/PrestoContainerTest.java +++ b/modules/presto/src/test/java/org/testcontainers/containers/PrestoContainerTest.java @@ -1,6 +1,6 @@ package org.testcontainers.containers; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.PrestoTestImages; import java.sql.Connection; @@ -14,10 +14,10 @@ import static org.assertj.core.api.Assertions.assertThat; -public class PrestoContainerTest { +class PrestoContainerTest { @Test - public void testSimple() throws Exception { + void testSimple() throws Exception { try (PrestoContainer prestoSql = new PrestoContainer<>(PrestoTestImages.PRESTO_TEST_IMAGE)) { prestoSql.start(); try ( @@ -35,7 +35,7 @@ public void testSimple() throws Exception { } @Test - public void testSpecificVersion() throws Exception { + void testSpecificVersion() throws Exception { try ( PrestoContainer prestoSql = new PrestoContainer<>(PrestoTestImages.PRESTO_PREVIOUS_VERSION_TEST_IMAGE) ) { @@ -54,7 +54,7 @@ public void testSpecificVersion() throws Exception { } @Test - public void testQueryMemoryAndTpch() throws SQLException { + void testQueryMemoryAndTpch() throws SQLException { try (PrestoContainer prestoSql = new PrestoContainer<>(PrestoTestImages.PRESTO_TEST_IMAGE)) { prestoSql.start(); try ( @@ -88,7 +88,7 @@ public void testQueryMemoryAndTpch() throws SQLException { } @Test - public void testInitScript() throws Exception { + void testInitScript() throws Exception { try (PrestoContainer prestoSql = new PrestoContainer<>(PrestoTestImages.PRESTO_TEST_IMAGE)) { prestoSql.withInitScript("initial.sql"); prestoSql.start(); @@ -105,7 +105,7 @@ public void testInitScript() throws Exception { } @Test - public void testTcJdbcUri() throws Exception { + void testTcJdbcUri() throws Exception { try ( Connection connection = DriverManager.getConnection( String.format("jdbc:tc:presto:%s://hostname/", PrestoContainer.DEFAULT_TAG) diff --git a/modules/presto/src/test/java/org/testcontainers/jdbc/presto/PrestoJDBCDriverTest.java b/modules/presto/src/test/java/org/testcontainers/jdbc/presto/PrestoJDBCDriverTest.java index 9c5dd712104..02e5b2a0485 100644 --- a/modules/presto/src/test/java/org/testcontainers/jdbc/presto/PrestoJDBCDriverTest.java +++ b/modules/presto/src/test/java/org/testcontainers/jdbc/presto/PrestoJDBCDriverTest.java @@ -1,16 +1,12 @@ package org.testcontainers.jdbc.presto; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; -@RunWith(Parameterized.class) -public class PrestoJDBCDriverTest extends AbstractJDBCDriverTest { +class PrestoJDBCDriverTest extends AbstractJDBCDriverTest { - @Parameterized.Parameters(name = "{index} - {0}") public static Iterable data() { return Arrays.asList( new Object[][] { // diff --git a/modules/pulsar/build.gradle b/modules/pulsar/build.gradle index 40edac13685..9dee1058aa5 100644 --- a/modules/pulsar/build.gradle +++ b/modules/pulsar/build.gradle @@ -3,7 +3,7 @@ description = "Testcontainers :: Pulsar" dependencies { api project(':testcontainers') - testImplementation group: 'org.apache.pulsar', name: 'pulsar-client', version: '3.1.2' - testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.25.1' - testImplementation group: 'org.apache.pulsar', name: 'pulsar-client-admin', version: '3.1.2' + testImplementation platform("org.apache.pulsar:pulsar-bom:4.2.0") + testImplementation 'org.apache.pulsar:pulsar-client' + testImplementation 'org.apache.pulsar:pulsar-client-admin' } diff --git a/modules/pulsar/src/main/java/org/testcontainers/containers/PulsarContainer.java b/modules/pulsar/src/main/java/org/testcontainers/containers/PulsarContainer.java index 0181d7144d9..7305fbd622c 100644 --- a/modules/pulsar/src/main/java/org/testcontainers/containers/PulsarContainer.java +++ b/modules/pulsar/src/main/java/org/testcontainers/containers/PulsarContainer.java @@ -7,26 +7,23 @@ /** * Testcontainers implementation for Apache Pulsar. *

    - * Supported image: {@code apachepulsar/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; - /** - * @deprecated The metrics endpoint is no longer being used for the WaitStrategy. - */ - @Deprecated - public static final String METRICS_ENDPOINT = "/metrics"; - private static final String ADMIN_CLUSTERS_ENDPOINT = "/admin/v2/clusters"; /** @@ -64,7 +61,7 @@ public PulsarContainer(String pulsarVersion) { public PulsarContainer(final DockerImageName dockerImageName) { super(dockerImageName); - dockerImageName.assertCompatibleWith(DockerImageName.parse("apachepulsar/pulsar")); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME, DockerImageName.parse("apachepulsar/pulsar-all")); withExposedPorts(BROKER_PORT, BROKER_HTTP_PORT); setWaitStrategy(waitAllStrategy); } diff --git a/modules/pulsar/src/main/java/org/testcontainers/pulsar/PulsarContainer.java b/modules/pulsar/src/main/java/org/testcontainers/pulsar/PulsarContainer.java new file mode 100644 index 00000000000..8936a7523c4 --- /dev/null +++ b/modules/pulsar/src/main/java/org/testcontainers/pulsar/PulsarContainer.java @@ -0,0 +1,103 @@ +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)); + } + } +} diff --git a/modules/pulsar/src/test/java/org/testcontainers/pulsar/AbstractPulsar.java b/modules/pulsar/src/test/java/org/testcontainers/pulsar/AbstractPulsar.java new file mode 100644 index 00000000000..c86b594b163 --- /dev/null +++ b/modules/pulsar/src/test/java/org/testcontainers/pulsar/AbstractPulsar.java @@ -0,0 +1,71 @@ +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"); + } +} diff --git a/modules/pulsar/src/test/java/org/testcontainers/pulsar/CompatibleApachePulsarImageTest.java b/modules/pulsar/src/test/java/org/testcontainers/pulsar/CompatibleApachePulsarImageTest.java new file mode 100644 index 00000000000..0661994466d --- /dev/null +++ b/modules/pulsar/src/test/java/org/testcontainers/pulsar/CompatibleApachePulsarImageTest.java @@ -0,0 +1,37 @@ +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()); + } + } +} diff --git a/modules/pulsar/src/test/java/org/testcontainers/containers/PulsarContainerTest.java b/modules/pulsar/src/test/java/org/testcontainers/pulsar/PulsarContainerTest.java similarity index 53% rename from modules/pulsar/src/test/java/org/testcontainers/containers/PulsarContainerTest.java rename to modules/pulsar/src/test/java/org/testcontainers/pulsar/PulsarContainerTest.java index 29321afca82..32d96c88453 100644 --- a/modules/pulsar/src/test/java/org/testcontainers/containers/PulsarContainerTest.java +++ b/modules/pulsar/src/test/java/org/testcontainers/pulsar/PulsarContainerTest.java @@ -1,38 +1,25 @@ -package org.testcontainers.containers; +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 org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.utility.DockerImageName; import java.time.Duration; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -public class PulsarContainerTest { - - public static final String TEST_TOPIC = "test_topic"; +class PulsarContainerTest extends AbstractPulsar { private static final DockerImageName PULSAR_IMAGE = DockerImageName.parse("apachepulsar/pulsar:3.0.0"); @Test - public void testUsage() throws Exception { + void testUsage() throws Exception { try ( // do not use PULSAR_IMAGE to make the doc looks easier // constructorWithVersion { - PulsarContainer pulsar = new PulsarContainer(DockerImageName.parse("apachepulsar/pulsar:3.0.0")); + PulsarContainer pulsar = new PulsarContainer("apachepulsar/pulsar:3.0.0"); // } ) { pulsar.start(); @@ -45,7 +32,7 @@ public void testUsage() throws Exception { } @Test - public void envVarsUsage() throws Exception { + void envVarsUsage() throws Exception { try ( // constructorWithEnv { PulsarContainer pulsar = new PulsarContainer(PULSAR_IMAGE) @@ -58,7 +45,7 @@ public void envVarsUsage() throws Exception { } @Test - public void customClusterName() throws Exception { + void customClusterName() throws Exception { try ( PulsarContainer pulsar = new PulsarContainer(PULSAR_IMAGE) .withEnv("PULSAR_PREFIX_clusterName", "tc-cluster"); @@ -69,7 +56,7 @@ public void customClusterName() throws Exception { } @Test - public void shouldNotEnableFunctionsWorkerByDefault() throws Exception { + void shouldNotEnableFunctionsWorkerByDefault() throws Exception { try (PulsarContainer pulsar = new PulsarContainer(PULSAR_IMAGE)) { pulsar.start(); @@ -81,10 +68,11 @@ public void shouldNotEnableFunctionsWorkerByDefault() throws Exception { } @Test - public void shouldWaitForFunctionsWorkerStarted() throws Exception { + void shouldWaitForFunctionsWorkerStarted() throws Exception { try ( // constructorWithFunctionsWorker { - PulsarContainer pulsar = new PulsarContainer(PULSAR_IMAGE).withFunctionsWorker(); + PulsarContainer pulsar = new PulsarContainer(DockerImageName.parse("apachepulsar/pulsar:3.0.0")) + .withFunctionsWorker(); // } ) { pulsar.start(); @@ -96,7 +84,7 @@ public void shouldWaitForFunctionsWorkerStarted() throws Exception { } @Test - public void testTransactions() throws Exception { + void testTransactions() throws Exception { try ( // constructorWithTransactions { PulsarContainer pulsar = new PulsarContainer(PULSAR_IMAGE).withTransactions(); @@ -111,15 +99,8 @@ public void testTransactions() throws Exception { } } - private void assertTransactionsTopicCreated(PulsarAdmin pulsarAdmin) throws PulsarAdminException { - final List topics = pulsarAdmin - .topics() - .getPartitionedTopicList("pulsar/system", ListTopicsOptions.builder().includeSystemTopic(true).build()); - assertThat(topics).contains("persistent://pulsar/system/transaction_coordinator_assign"); - } - @Test - public void testTransactionsAndFunctionsWorker() throws Exception { + void testTransactionsAndFunctionsWorker() throws Exception { try (PulsarContainer pulsar = new PulsarContainer(PULSAR_IMAGE).withTransactions().withFunctionsWorker()) { pulsar.start(); @@ -132,7 +113,7 @@ public void testTransactionsAndFunctionsWorker() throws Exception { } @Test - public void testClusterFullyInitialized() throws Exception { + void testClusterFullyInitialized() throws Exception { try (PulsarContainer pulsar = new PulsarContainer(PULSAR_IMAGE)) { pulsar.start(); @@ -143,47 +124,10 @@ public void testClusterFullyInitialized() throws Exception { } @Test - public void testStartupTimeoutIsHonored() { + void testStartupTimeoutIsHonored() { try (PulsarContainer pulsar = new PulsarContainer(PULSAR_IMAGE).withStartupTimeout(Duration.ZERO)) { assertThatThrownBy(pulsar::start) .hasRootCauseMessage("Precondition failed: timeout must be greater than zero"); } } - - protected void testPulsarFunctionality(String pulsarBrokerUrl) throws Exception { - try ( - PulsarClient client = PulsarClient.builder().serviceUrl(pulsarBrokerUrl).build(); - Consumer consumer = client.newConsumer().topic(TEST_TOPIC).subscriptionName("test-subs").subscribe(); - Producer producer = client.newProducer().topic(TEST_TOPIC).create() - ) { - producer.send("test containers".getBytes()); - CompletableFuture future = consumer.receiveAsync(); - Message message = future.get(5, TimeUnit.SECONDS); - - assertThat(new String(message.getData())).isEqualTo("test containers"); - } - } - - protected void testTransactionFunctionality(String pulsarBrokerUrl) throws Exception { - try ( - PulsarClient client = PulsarClient.builder().serviceUrl(pulsarBrokerUrl).enableTransaction(true).build(); - Consumer consumer = client - .newConsumer(Schema.STRING) - .topic("transaction-topic") - .subscriptionInitialPosition(SubscriptionInitialPosition.Earliest) - .subscriptionName("test-transaction-sub") - .subscribe(); - Producer producer = client - .newProducer(Schema.STRING) - .sendTimeout(0, TimeUnit.SECONDS) - .topic("transaction-topic") - .create() - ) { - final Transaction transaction = client.newTransaction().build().get(); - producer.newMessage(transaction).value("first").send(); - transaction.commit(); - Message message = consumer.receive(); - assertThat(message.getValue()).isEqualTo("first"); - } - } } diff --git a/modules/qdrant/build.gradle b/modules/qdrant/build.gradle new file mode 100644 index 00000000000..98b8fc403fc --- /dev/null +++ b/modules/qdrant/build.gradle @@ -0,0 +1,11 @@ +description = "Testcontainers :: Qdrant" + +dependencies { + api project(':testcontainers') + + testImplementation 'io.qdrant:client:1.17.0' + testImplementation platform('io.grpc:grpc-bom:1.80.0') + testImplementation 'io.grpc:grpc-stub' + testImplementation 'io.grpc:grpc-protobuf' + testImplementation 'io.grpc:grpc-netty-shaded' +} diff --git a/modules/qdrant/src/main/java/org/testcontainers/qdrant/QdrantContainer.java b/modules/qdrant/src/main/java/org/testcontainers/qdrant/QdrantContainer.java new file mode 100644 index 00000000000..e00ac541d48 --- /dev/null +++ b/modules/qdrant/src/main/java/org/testcontainers/qdrant/QdrantContainer.java @@ -0,0 +1,57 @@ +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(); + } +} diff --git a/modules/qdrant/src/test/java/org/testcontainers/qdrant/QdrantContainerTest.java b/modules/qdrant/src/test/java/org/testcontainers/qdrant/QdrantContainerTest.java new file mode 100644 index 00000000000..724edf8555c --- /dev/null +++ b/modules/qdrant/src/test/java/org/testcontainers/qdrant/QdrantContainerTest.java @@ -0,0 +1,89 @@ +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(); + } + } +} diff --git a/modules/qdrant/src/test/resources/logback-test.xml b/modules/qdrant/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..83ef7a1a3ef --- /dev/null +++ b/modules/qdrant/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + diff --git a/modules/questdb/build.gradle b/modules/questdb/build.gradle index 1850ed0fe34..04b714ff783 100644 --- a/modules/questdb/build.gradle +++ b/modules/questdb/build.gradle @@ -2,25 +2,12 @@ description = "Testcontainers :: QuestDB" dependencies { api project(':testcontainers') - api project(':jdbc') + api project(':testcontainers-jdbc') - testRuntimeOnly 'org.postgresql:postgresql:42.7.1' - testImplementation project(':jdbc-test') - testImplementation 'org.assertj:assertj-core:3.25.2' - testImplementation 'org.questdb:questdb:7.3.9' - testImplementation 'org.awaitility:awaitility:4.2.0' - testImplementation 'org.apache.httpcomponents:httpclient:4.5.14' -} + testRuntimeOnly 'org.postgresql:postgresql:42.7.12' -test { - javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(11) - } -} - -compileTestJava { - javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(11) - } - options.release.set(11) + 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' } diff --git a/modules/questdb/src/test/java/org/testcontainers/QuestDBTestImages.java b/modules/questdb/src/test/java/org/testcontainers/QuestDBTestImages.java index 555b56f172c..14e42d72fae 100644 --- a/modules/questdb/src/test/java/org/testcontainers/QuestDBTestImages.java +++ b/modules/questdb/src/test/java/org/testcontainers/QuestDBTestImages.java @@ -3,5 +3,5 @@ import org.testcontainers.utility.DockerImageName; public interface QuestDBTestImages { - DockerImageName QUESTDB_IMAGE = DockerImageName.parse("questdb/questdb:6.5.3"); + DockerImageName QUESTDB_IMAGE = DockerImageName.parse("questdb/questdb:9.2.2"); } diff --git a/modules/questdb/src/test/java/org/testcontainers/jdbc/questdb/QuestDBJDBCDriverTest.java b/modules/questdb/src/test/java/org/testcontainers/jdbc/questdb/QuestDBJDBCDriverTest.java index 3144f8bfad3..40612f05680 100644 --- a/modules/questdb/src/test/java/org/testcontainers/jdbc/questdb/QuestDBJDBCDriverTest.java +++ b/modules/questdb/src/test/java/org/testcontainers/jdbc/questdb/QuestDBJDBCDriverTest.java @@ -1,16 +1,12 @@ package org.testcontainers.jdbc.questdb; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; -@RunWith(Parameterized.class) -public class QuestDBJDBCDriverTest extends AbstractJDBCDriverTest { +class QuestDBJDBCDriverTest extends AbstractJDBCDriverTest { - @Parameterized.Parameters(name = "{index} - {0}") public static Iterable data() { return Arrays.asList( new Object[][] { diff --git a/modules/questdb/src/test/java/org/testcontainers/junit/questdb/SimpleQuestDBTest.java b/modules/questdb/src/test/java/org/testcontainers/junit/questdb/SimpleQuestDBTest.java index c38524018c3..6b0ff6e3107 100644 --- a/modules/questdb/src/test/java/org/testcontainers/junit/questdb/SimpleQuestDBTest.java +++ b/modules/questdb/src/test/java/org/testcontainers/junit/questdb/SimpleQuestDBTest.java @@ -6,7 +6,7 @@ import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.QuestDBTestImages; import org.testcontainers.containers.QuestDBContainer; import org.testcontainers.db.AbstractContainerDatabaseTest; @@ -20,13 +20,16 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -public class SimpleQuestDBTest extends AbstractContainerDatabaseTest { +class SimpleQuestDBTest extends AbstractContainerDatabaseTest { private static final String TABLE_NAME = "mytable"; @Test - public void testSimple() throws SQLException { - try (QuestDBContainer questDB = new QuestDBContainer(QuestDBTestImages.QUESTDB_IMAGE)) { + void testSimple() throws SQLException { + try ( // container { + QuestDBContainer questDB = new QuestDBContainer("questdb/questdb:9.2.2") + // } + ) { questDB.start(); ResultSet resultSet = performQuery(questDB, questDB.getTestQueryString()); @@ -37,7 +40,7 @@ public void testSimple() throws SQLException { } @Test - public void testRest() throws IOException { + void testRest() throws IOException { try (QuestDBContainer questdb = new QuestDBContainer(QuestDBTestImages.QUESTDB_IMAGE)) { questdb.start(); populateByInfluxLineProtocol(questdb, 1_000); @@ -57,7 +60,7 @@ public void testRest() throws IOException { } private static void populateByInfluxLineProtocol(QuestDBContainer questdb, int rowCount) { - try (Sender sender = Sender.builder().address(questdb.getIlpUrl()).build()) { + try (Sender sender = Sender.builder(Sender.Transport.TCP).address(questdb.getIlpUrl()).build()) { for (int i = 0; i < rowCount; i++) { sender .table(TABLE_NAME) diff --git a/modules/r2dbc/build.gradle b/modules/r2dbc/build.gradle index d6d5fcc267d..9c93885fd6f 100644 --- a/modules/r2dbc/build.gradle +++ b/modules/r2dbc/build.gradle @@ -5,16 +5,13 @@ plugins { description = "Testcontainers :: R2DBC" dependencies { - annotationProcessor 'com.google.auto.service:auto-service:1.1.1' - compileOnly 'com.google.auto.service:auto-service:1.1.1' - api project(':testcontainers') api 'io.r2dbc:r2dbc-spi:0.9.0.RELEASE' - testImplementation 'org.assertj:assertj-core:3.25.2' testImplementation 'io.r2dbc:r2dbc-postgresql:0.8.13.RELEASE' - testImplementation project(':postgresql') + testImplementation project(':testcontainers-postgresql') - testFixturesImplementation 'io.projectreactor:reactor-core:3.6.2' - testFixturesImplementation 'org.assertj:assertj-core:3.25.2' + testFixturesImplementation 'io.projectreactor:reactor-core:3.8.6' + testFixturesImplementation 'org.assertj:assertj-core:3.27.7' + testFixturesImplementation 'org.junit.jupiter:junit-jupiter:5.14.3' } diff --git a/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/Hidden.java b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/Hidden.java index 5d9de2025a0..327ad6ab9ca 100644 --- a/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/Hidden.java +++ b/modules/r2dbc/src/main/java/org/testcontainers/r2dbc/Hidden.java @@ -1,6 +1,5 @@ package org.testcontainers.r2dbc; -import com.google.auto.service.AutoService; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.ConnectionFactoryOptions; import io.r2dbc.spi.ConnectionFactoryProvider; @@ -10,7 +9,6 @@ */ class Hidden { - @AutoService(ConnectionFactoryProvider.class) public static final class TestcontainersR2DBCConnectionFactoryProvider implements ConnectionFactoryProvider { public static final String DRIVER = "tc"; diff --git a/modules/r2dbc/src/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider b/modules/r2dbc/src/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider new file mode 100644 index 00000000000..23f1702c57a --- /dev/null +++ b/modules/r2dbc/src/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider @@ -0,0 +1 @@ +org.testcontainers.r2dbc.Hidden$TestcontainersR2DBCConnectionFactoryProvider diff --git a/modules/r2dbc/src/test/java/org/testcontainers/r2dbc/TestcontainersR2DBCConnectionFactoryTest.java b/modules/r2dbc/src/test/java/org/testcontainers/r2dbc/TestcontainersR2DBCConnectionFactoryTest.java index 9ae49b00bd1..7a76b436e22 100644 --- a/modules/r2dbc/src/test/java/org/testcontainers/r2dbc/TestcontainersR2DBCConnectionFactoryTest.java +++ b/modules/r2dbc/src/test/java/org/testcontainers/r2dbc/TestcontainersR2DBCConnectionFactoryTest.java @@ -6,7 +6,7 @@ import io.r2dbc.spi.ConnectionFactories; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.Result; -import org.junit.Test; +import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -15,10 +15,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -public class TestcontainersR2DBCConnectionFactoryTest { +class TestcontainersR2DBCConnectionFactoryTest { @Test - public void failsOnUnknownProvider() { + void failsOnUnknownProvider() { String nonExistingProvider = UUID.randomUUID().toString(); assertThatThrownBy(() -> { ConnectionFactories.get(String.format("r2dbc:tc:%s:///db", nonExistingProvider)); @@ -28,7 +28,7 @@ public void failsOnUnknownProvider() { } @Test - public void reusesUntilConnectionFactoryIsClosed() { + void reusesUntilConnectionFactoryIsClosed() { String url = "r2dbc:tc:postgresql:///db?TC_IMAGE_TAG=10-alpine"; ConnectionFactory connectionFactory = ConnectionFactories.get(url); diff --git a/modules/r2dbc/src/testFixtures/java/org/testcontainers/r2dbc/AbstractR2DBCDatabaseContainerTest.java b/modules/r2dbc/src/testFixtures/java/org/testcontainers/r2dbc/AbstractR2DBCDatabaseContainerTest.java index ca06e22d631..ed0a7fba70f 100644 --- a/modules/r2dbc/src/testFixtures/java/org/testcontainers/r2dbc/AbstractR2DBCDatabaseContainerTest.java +++ b/modules/r2dbc/src/testFixtures/java/org/testcontainers/r2dbc/AbstractR2DBCDatabaseContainerTest.java @@ -6,7 +6,7 @@ import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.ConnectionFactoryMetadata; import io.r2dbc.spi.ConnectionFactoryOptions; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -28,7 +28,7 @@ protected String createTestQuery(int result) { } @Test - public final void testGetOptions() { + void testGetOptions() { try (T container = createContainer()) { container.start(); @@ -38,13 +38,13 @@ public final void testGetOptions() { } @Test - public final void testUrlSupport() { + void testUrlSupport() { ConnectionFactory connectionFactory = ConnectionFactories.get(createR2DBCUrl()); runTestQuery(connectionFactory); } @Test - public final void testGetMetadata() { + void testGetMetadata() { ConnectionFactory connectionFactory = ConnectionFactories.get(createR2DBCUrl()); ConnectionFactoryMetadata metadata = connectionFactory.getMetadata(); assertThat(metadata).isNotNull(); diff --git a/modules/rabbitmq/build.gradle b/modules/rabbitmq/build.gradle index 020b8f5b449..1b9cd99168b 100644 --- a/modules/rabbitmq/build.gradle +++ b/modules/rabbitmq/build.gradle @@ -2,7 +2,7 @@ description = "Testcontainers :: RabbitMQ" dependencies { api project(":testcontainers") - testImplementation 'com.rabbitmq:amqp-client:5.20.0' - testImplementation 'org.assertj:assertj-core:3.25.1' - compileOnly 'org.jetbrains:annotations:24.1.0' + + testImplementation 'com.rabbitmq:amqp-client:5.33.0' + compileOnly 'org.jetbrains:annotations:26.1.0' } diff --git a/modules/rabbitmq/src/main/java/org/testcontainers/containers/RabbitMQContainer.java b/modules/rabbitmq/src/main/java/org/testcontainers/containers/RabbitMQContainer.java index bac998b85ac..6db9f6b4cf0 100644 --- a/modules/rabbitmq/src/main/java/org/testcontainers/containers/RabbitMQContainer.java +++ b/modules/rabbitmq/src/main/java/org/testcontainers/containers/RabbitMQContainer.java @@ -9,7 +9,6 @@ import org.testcontainers.utility.MountableFile; import java.io.IOException; -import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -28,7 +27,10 @@ *
  • 15671 (HTTPS)
  • *
  • 15672 (HTTP)
  • * + * + * @deprecated use {@link org.testcontainers.rabbitmq.RabbitMQContainer} instead. */ +@Deprecated public class RabbitMQContainer extends GenericContainer { /** @@ -76,14 +78,16 @@ public RabbitMQContainer(final DockerImageName dockerImageName) { addExposedPorts(DEFAULT_AMQP_PORT, DEFAULT_AMQPS_PORT, DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT); - this.waitStrategy = - Wait.forLogMessage(".*Server startup complete.*", 1).withStartupTimeout(Duration.ofSeconds(60)); + waitingFor(Wait.forLogMessage(".*Server startup complete.*", 1)); } @Override protected void configure() { - if (adminPassword != null) { - addEnv("RABBITMQ_DEFAULT_PASS", adminPassword); + if (this.adminUsername != null) { + addEnv("RABBITMQ_DEFAULT_USER", this.adminUsername); + } + if (this.adminPassword != null) { + addEnv("RABBITMQ_DEFAULT_PASS", this.adminPassword); } } @@ -105,11 +109,14 @@ protected void containerIsStarted(InspectContainerResponse containerInfo) { * @return The admin password for the admin account */ public String getAdminPassword() { - return adminPassword; + return this.adminPassword; } + /** + * @return The admin user for the admin account + */ public String getAdminUsername() { - return adminUsername; + return this.adminUsername; } public Integer getAmqpPort() { @@ -156,6 +163,17 @@ public String getHttpsUrl() { return "https://" + getHost() + ":" + getHttpsPort(); } + /** + * Sets the user for the admin (default is
    guest
    ) + * + * @param adminUsername The admin user. + * @return This container. + */ + public RabbitMQContainer withAdminUser(final String adminUsername) { + this.adminUsername = adminUsername; + return this; + } + /** * Sets the password for the admin (default is
    guest
    ) * diff --git a/modules/rabbitmq/src/main/java/org/testcontainers/rabbitmq/RabbitMQContainer.java b/modules/rabbitmq/src/main/java/org/testcontainers/rabbitmq/RabbitMQContainer.java new file mode 100644 index 00000000000..5b6d79d73c4 --- /dev/null +++ b/modules/rabbitmq/src/main/java/org/testcontainers/rabbitmq/RabbitMQContainer.java @@ -0,0 +1,204 @@ +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"); + } +} diff --git a/modules/rabbitmq/src/test/java/org/testcontainers/containers/RabbitMQContainerJUnitIntegrationTest.java b/modules/rabbitmq/src/test/java/org/testcontainers/containers/RabbitMQContainerJUnitIntegrationTest.java deleted file mode 100644 index e04ab616e66..00000000000 --- a/modules/rabbitmq/src/test/java/org/testcontainers/containers/RabbitMQContainerJUnitIntegrationTest.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.testcontainers.containers; - -import org.junit.ClassRule; -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Test for basic functionality when used as a @ClassRule. - */ -public class RabbitMQContainerJUnitIntegrationTest { - - @ClassRule - public static RabbitMQContainer rabbitMQContainer = new RabbitMQContainer(RabbitMQTestImages.RABBITMQ_IMAGE); - - @Test - public void shouldStart() { - assertThat(rabbitMQContainer.isRunning()).isTrue(); - } -} diff --git a/modules/rabbitmq/src/test/java/org/testcontainers/containers/RabbitMQContainerTest.java b/modules/rabbitmq/src/test/java/org/testcontainers/containers/RabbitMQContainerTest.java deleted file mode 100644 index d26b4d6e924..00000000000 --- a/modules/rabbitmq/src/test/java/org/testcontainers/containers/RabbitMQContainerTest.java +++ /dev/null @@ -1,304 +0,0 @@ -package org.testcontainers.containers; - -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; -import org.junit.Test; -import org.testcontainers.containers.RabbitMQContainer.SslVerification; -import org.testcontainers.utility.MountableFile; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.UnrecoverableKeyException; -import java.security.cert.CertificateException; -import java.util.Collections; - -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.assertThatCode; - -public 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 - public void shouldCreateRabbitMQContainer() { - try (RabbitMQContainer container = new RabbitMQContainer(RabbitMQTestImages.RABBITMQ_IMAGE)) { - assertThat(container.getAdminPassword()).isEqualTo("guest"); - assertThat(container.getAdminUsername()).isEqualTo("guest"); - - container.start(); - - assertThat(container.getAmqpsUrl()) - .isEqualTo( - String.format("amqps://%s:%d", container.getHost(), container.getMappedPort(DEFAULT_AMQPS_PORT)) - ); - assertThat(container.getAmqpUrl()) - .isEqualTo( - String.format("amqp://%s:%d", container.getHost(), container.getMappedPort(DEFAULT_AMQP_PORT)) - ); - assertThat(container.getHttpsUrl()) - .isEqualTo( - String.format("https://%s:%d", container.getHost(), container.getMappedPort(DEFAULT_HTTPS_PORT)) - ); - assertThat(container.getHttpUrl()) - .isEqualTo( - String.format("http://%s:%d", container.getHost(), container.getMappedPort(DEFAULT_HTTP_PORT)) - ); - - assertThat(container.getHttpsPort()).isEqualTo(container.getMappedPort(DEFAULT_HTTPS_PORT)); - assertThat(container.getHttpPort()).isEqualTo(container.getMappedPort(DEFAULT_HTTP_PORT)); - assertThat(container.getAmqpsPort()).isEqualTo(container.getMappedPort(DEFAULT_AMQPS_PORT)); - assertThat(container.getAmqpPort()).isEqualTo(container.getMappedPort(DEFAULT_AMQP_PORT)); - - assertThat(container.getLivenessCheckPortNumbers()) - .containsExactlyInAnyOrder( - container.getMappedPort(DEFAULT_AMQP_PORT), - container.getMappedPort(DEFAULT_AMQPS_PORT), - container.getMappedPort(DEFAULT_HTTP_PORT), - container.getMappedPort(DEFAULT_HTTPS_PORT) - ); - } - } - - @Test - public void shouldCreateRabbitMQContainerWithExchange() throws IOException, InterruptedException { - try (RabbitMQContainer container = new RabbitMQContainer(RabbitMQTestImages.RABBITMQ_IMAGE)) { - container.withExchange("test-exchange", "direct"); - - container.start(); - - assertThat(container.execInContainer("rabbitmqctl", "list_exchanges").getStdout()) - .containsPattern("test-exchange\\s+direct"); - } - } - - @Test - public void shouldCreateRabbitMQContainerWithExchangeInVhost() throws IOException, InterruptedException { - try (RabbitMQContainer container = new RabbitMQContainer(RabbitMQTestImages.RABBITMQ_IMAGE)) { - container.withVhost("test-vhost"); - container.withExchange( - "test-vhost", - "test-exchange", - "direct", - false, - false, - false, - Collections.emptyMap() - ); - - container.start(); - - assertThat(container.execInContainer("rabbitmqctl", "list_exchanges", "-p", "test-vhost").getStdout()) - .containsPattern("test-exchange\\s+direct"); - } - } - - @Test - public void shouldCreateRabbitMQContainerWithQueues() throws IOException, InterruptedException { - try (RabbitMQContainer container = new RabbitMQContainer(RabbitMQTestImages.RABBITMQ_IMAGE)) { - container - .withQueue("queue-one") - .withQueue("queue-two", false, true, ImmutableMap.of("x-message-ttl", 1000)); - - container.start(); - - assertThat(container.execInContainer("rabbitmqctl", "list_queues", "name", "arguments").getStdout()) - .containsPattern("queue-one"); - assertThat(container.execInContainer("rabbitmqctl", "list_queues", "name", "arguments").getStdout()) - .containsPattern("queue-two\\s.*x-message-ttl"); - } - } - - @Test - public 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 - public 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 - public 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` - } - } - - @Test - public void shouldStartTheWholeEnchilada() throws IOException, InterruptedException { - try (RabbitMQContainer container = new RabbitMQContainer(RabbitMQTestImages.RABBITMQ_IMAGE)) { - container - .withVhost("vhost1") - .withVhostLimit("vhost1", "max-connections", 1) - .withVhost("vhost2", true) - .withExchange("direct-exchange", "direct") - .withExchange("topic-exchange", "topic") - .withExchange("vhost1", "topic-exchange-2", "topic", false, false, true, Collections.emptyMap()) - .withExchange("vhost2", "topic-exchange-3", "topic") - .withExchange("topic-exchange-4", "topic", false, false, true, Collections.emptyMap()) - .withQueue("queue1") - .withQueue("queue2", true, false, ImmutableMap.of("x-message-ttl", 1000)) - .withQueue("vhost1", "queue3", true, false, ImmutableMap.of("x-message-ttl", 1000)) - .withQueue("vhost2", "queue4") - .withBinding("direct-exchange", "queue1") - .withBinding("vhost1", "topic-exchange-2", "queue3") - .withBinding("vhost2", "topic-exchange-3", "queue4", Collections.emptyMap(), "ss7", "queue") - .withUser("user1", "password1") - .withUser("user2", "password2", ImmutableSet.of("administrator")) - .withPermission("vhost1", "user1", ".*", ".*", ".*") - .withPolicy("max length policy", "^dog", ImmutableMap.of("max-length", 1), 1, "queues") - .withPolicy( - "alternate exchange policy", - "^direct-exchange", - ImmutableMap.of("alternate-exchange", "amq.direct") - ) - .withPolicy("vhost2", "ha-all", ".*", ImmutableMap.of("ha-mode", "all", "ha-sync-mode", "automatic")) - .withOperatorPolicy("operator policy 1", "^queue1", ImmutableMap.of("message-ttl", 1000), 1, "queues") - .withPluginsEnabled("rabbitmq_shovel", "rabbitmq_random_exchange"); - - container.start(); - - assertThat(container.execInContainer("rabbitmqadmin", "list", "queues").getStdout()) - .contains("queue1", "queue2", "queue3", "queue4"); - - assertThat(container.execInContainer("rabbitmqadmin", "list", "exchanges").getStdout()) - .contains( - "direct-exchange", - "topic-exchange", - "topic-exchange-2", - "topic-exchange-3", - "topic-exchange-4" - ); - - assertThat(container.execInContainer("rabbitmqadmin", "list", "bindings").getStdout()) - .contains("direct-exchange", "topic-exchange-2", "topic-exchange-3"); - - assertThat(container.execInContainer("rabbitmqadmin", "list", "users").getStdout()) - .contains("user1", "user2"); - - assertThat(container.execInContainer("rabbitmqadmin", "list", "policies").getStdout()) - .contains("max length policy", "alternate exchange policy"); - - assertThat(container.execInContainer("rabbitmqadmin", "list", "policies", "--vhost=vhost2").getStdout()) - .contains("ha-all", "ha-sync-mode"); - - assertThat(container.execInContainer("rabbitmqadmin", "list", "operator_policies").getStdout()) - .contains("operator policy 1"); - - assertThat( - container.execInContainer("rabbitmq-plugins", "is_enabled", "rabbitmq_shovel", "--quiet").getStdout() - ) - .contains("rabbitmq_shovel is enabled"); - - assertThat( - container - .execInContainer("rabbitmq-plugins", "is_enabled", "rabbitmq_random_exchange", "--quiet") - .getStdout() - ) - .contains("rabbitmq_random_exchange is enabled"); - } - } - - @Test - public void shouldThrowExceptionForDodgyJson() { - try (RabbitMQContainer container = new RabbitMQContainer(RabbitMQTestImages.RABBITMQ_IMAGE)) { - assertThatCode(() -> container.withQueue("queue2", true, false, ImmutableMap.of("x-message-ttl", container)) - ) - .hasMessageStartingWith("Failed to convert arguments into json"); - } - } - - @Test - public void shouldWorkWithSSL() { - try (RabbitMQContainer container = new RabbitMQContainer(RabbitMQTestImages.RABBITMQ_IMAGE)) { - container.withSSL( - MountableFile.forClasspathResource("/certs/server_key.pem", 0644), - MountableFile.forClasspathResource("/certs/server_certificate.pem", 0644), - MountableFile.forClasspathResource("/certs/ca_certificate.pem", 0644), - SslVerification.VERIFY_PEER, - true - ); - - container.start(); - - assertThatCode(() -> { - ConnectionFactory connectionFactory = new ConnectionFactory(); - connectionFactory.useSslProtocol( - createSslContext("certs/client_key.p12", "password", "certs/truststore.jks", "password") - ); - connectionFactory.enableHostnameVerification(); - connectionFactory.setUri(container.getAmqpsUrl()); - connectionFactory.setPassword(container.getAdminPassword()); - Connection connection = connectionFactory.newConnection(); - Channel channel = connection - .openChannel() - .orElseThrow(() -> new RuntimeException("Failed to Open channel")); - channel.close(); - connection.close(); - }) - .doesNotThrowAnyException(); - } - } - - private SSLContext createSslContext( - String keystoreFile, - String keystorePassword, - String truststoreFile, - String truststorePassword - ) - throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException, KeyManagementException { - ClassLoader classLoader = getClass().getClassLoader(); - - KeyStore ks = KeyStore.getInstance("PKCS12"); - ks.load( - new FileInputStream(new File(classLoader.getResource(keystoreFile).getFile())), - keystorePassword.toCharArray() - ); - KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); - kmf.init(ks, "password".toCharArray()); - - KeyStore trustStore = KeyStore.getInstance("PKCS12"); - trustStore.load( - new FileInputStream(new File(classLoader.getResource(truststoreFile).getFile())), - truststorePassword.toCharArray() - ); - TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); - tmf.init(trustStore); - - SSLContext c = SSLContext.getInstance("TLSv1.2"); - c.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); - return c; - } -} diff --git a/modules/rabbitmq/src/test/java/org/testcontainers/rabbitmq/RabbitMQContainerTest.java b/modules/rabbitmq/src/test/java/org/testcontainers/rabbitmq/RabbitMQContainerTest.java new file mode 100644 index 00000000000..b3434b2d69d --- /dev/null +++ b/modules/rabbitmq/src/test/java/org/testcontainers/rabbitmq/RabbitMQContainerTest.java @@ -0,0 +1,123 @@ +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); + } + } +} diff --git a/modules/rabbitmq/src/test/java/org/testcontainers/containers/RabbitMQTestImages.java b/modules/rabbitmq/src/test/java/org/testcontainers/rabbitmq/RabbitMQTestImages.java similarity index 82% rename from modules/rabbitmq/src/test/java/org/testcontainers/containers/RabbitMQTestImages.java rename to modules/rabbitmq/src/test/java/org/testcontainers/rabbitmq/RabbitMQTestImages.java index 0898cbdb350..56294f1c196 100644 --- a/modules/rabbitmq/src/test/java/org/testcontainers/containers/RabbitMQTestImages.java +++ b/modules/rabbitmq/src/test/java/org/testcontainers/rabbitmq/RabbitMQTestImages.java @@ -1,4 +1,4 @@ -package org.testcontainers.containers; +package org.testcontainers.rabbitmq; import org.testcontainers.utility.DockerImageName; diff --git a/modules/redpanda/build.gradle b/modules/redpanda/build.gradle index 0690bcde61c..40c88ea347e 100644 --- a/modules/redpanda/build.gradle +++ b/modules/redpanda/build.gradle @@ -2,9 +2,9 @@ description = "Testcontainers :: Redpanda" dependencies { api project(':testcontainers') - shaded 'org.freemarker:freemarker:2.3.32' + shaded 'org.freemarker:freemarker:2.3.34' - testImplementation 'org.apache.kafka:kafka-clients:3.6.1' - testImplementation 'org.assertj:assertj-core:3.25.1' - testImplementation 'io.rest-assured:rest-assured:5.4.0' + testImplementation 'org.apache.kafka:kafka-clients:4.3.1' + testImplementation 'io.rest-assured:rest-assured:5.5.7' + testImplementation 'org.awaitility:awaitility:4.3.0' } diff --git a/modules/redpanda/src/main/java/org/testcontainers/redpanda/RedpandaContainer.java b/modules/redpanda/src/main/java/org/testcontainers/redpanda/RedpandaContainer.java index cc1d07c883a..e8655beec45 100644 --- a/modules/redpanda/src/main/java/org/testcontainers/redpanda/RedpandaContainer.java +++ b/modules/redpanda/src/main/java/org/testcontainers/redpanda/RedpandaContainer.java @@ -45,16 +45,10 @@ public class RedpandaContainer extends GenericContainer { private static final String IMAGE_NAME = "redpandadata/redpanda"; - @Deprecated - private static final String REDPANDA_OLD_FULL_IMAGE_NAME = "docker.redpanda.com/vectorized/redpanda"; - private static final DockerImageName REDPANDA_IMAGE = DockerImageName.parse(REDPANDA_FULL_IMAGE_NAME); private static final DockerImageName IMAGE = DockerImageName.parse(IMAGE_NAME); - @Deprecated - private static final DockerImageName REDPANDA_OLD_IMAGE = DockerImageName.parse(REDPANDA_OLD_FULL_IMAGE_NAME); - private static final int REDPANDA_PORT = 9092; private static final int REDPANDA_ADMIN_PORT = 9644; @@ -71,28 +65,30 @@ public class RedpandaContainer extends GenericContainer { 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_OLD_IMAGE, REDPANDA_IMAGE, IMAGE); + imageName.assertCompatibleWith(REDPANDA_IMAGE, IMAGE); boolean isLessThanBaseVersion = new ComparableVersion(imageName.getVersionPart()).isLessThan("v22.2.1"); boolean isPublicCompatibleImage = REDPANDA_FULL_IMAGE_NAME.equals(imageName.getUnversionedPart()) || - IMAGE_NAME.equals(imageName.getUnversionedPart()) || - REDPANDA_OLD_FULL_IMAGE_NAME.equals(imageName.getUnversionedPart()); + IMAGE_NAME.equals(imageName.getUnversionedPart()); if (isPublicCompatibleImage && isLessThanBaseVersion) { throw new IllegalArgumentException("Redpanda version must be >= v22.2.1"); } withExposedPorts(REDPANDA_PORT, REDPANDA_ADMIN_PORT, SCHEMA_REGISTRY_PORT, REST_PROXY_PORT); withCreateContainerCmdModifier(cmd -> { - cmd.withEntrypoint(); + cmd.withEntrypoint("/entrypoint-tc.sh"); cmd.withUser("root:root"); }); waitingFor(Wait.forLogMessage(".*Successfully started Redpanda!.*", 1)); @@ -100,7 +96,7 @@ public RedpandaContainer(DockerImageName imageName) { MountableFile.forClasspathResource("testcontainers/entrypoint-tc.sh", 0700), "/entrypoint-tc.sh" ); - withCommand("/entrypoint-tc.sh", "redpanda", "start", "--mode=dev-container", "--smp=1", "--memory=1G"); + withCommand("redpanda", "start", "--mode=dev-container", "--smp=1", "--memory=1G"); } @Override @@ -109,6 +105,7 @@ protected void configure() { .map(Supplier::get) .map(Listener::getAddress) .forEach(this::withNetworkAliases); + this.listeners.keySet().stream().map(listener -> listener.split(":")[0]).forEach(this::withNetworkAliases); } @SneakyThrows @@ -212,13 +209,74 @@ public RedpandaContainer withSuperuser(String username) { * * @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); @@ -233,6 +291,12 @@ private Transferable getBootstrapFile(Configuration cfg) { } 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) @@ -244,19 +308,44 @@ private Transferable getRedpandaFile(Configuration cfg) { return listenerMap; }) .collect(Collectors.toList()); - - Map kafkaApi = new HashMap<>(); - kafkaApi.put("authenticationMethod", this.authenticationMethod); - kafkaApi.put("enableAuthorization", this.enableAuthorization); - kafkaApi.put("advertisedHost", getHost()); - kafkaApi.put("advertisedPort", getMappedPort(9092)); 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); diff --git a/modules/redpanda/src/main/resources/testcontainers/bootstrap.yaml.ftl b/modules/redpanda/src/main/resources/testcontainers/bootstrap.yaml.ftl index f066cf428aa..616415c359e 100644 --- a/modules/redpanda/src/main/resources/testcontainers/bootstrap.yaml.ftl +++ b/modules/redpanda/src/main/resources/testcontainers/bootstrap.yaml.ftl @@ -1,7 +1,7 @@ # 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 Admi n API. +# properties via the Redpanda Admin API. superusers: <#if kafkaApi.superusers?has_content > <#list kafkaApi.superusers as superuser> diff --git a/modules/redpanda/src/main/resources/testcontainers/redpanda.yaml.ftl b/modules/redpanda/src/main/resources/testcontainers/redpanda.yaml.ftl index 457a3738652..80fb3c09418 100644 --- a/modules/redpanda/src/main/resources/testcontainers/redpanda.yaml.ftl +++ b/modules/redpanda/src/main/resources/testcontainers/redpanda.yaml.ftl @@ -27,6 +27,12 @@ redpanda: 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 } @@ -40,6 +46,11 @@ redpanda: 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: diff --git a/modules/redpanda/src/test/java/org/testcontainers/redpanda/AbstractRedpanda.java b/modules/redpanda/src/test/java/org/testcontainers/redpanda/AbstractRedpanda.java index 35998e8e8fa..ce985a59fdb 100644 --- a/modules/redpanda/src/test/java/org/testcontainers/redpanda/AbstractRedpanda.java +++ b/modules/redpanda/src/test/java/org/testcontainers/redpanda/AbstractRedpanda.java @@ -13,7 +13,7 @@ import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.common.serialization.StringDeserializer; import org.apache.kafka.common.serialization.StringSerializer; -import org.rnorth.ducttape.unreliables.Unreliables; +import org.awaitility.Awaitility; import java.time.Duration; import java.util.Collection; @@ -67,24 +67,17 @@ protected void testKafkaFunctionality(String bootstrapServers, int partitions, i producer.send(new ProducerRecord<>(topicName, "testcontainers", "rulezzz")).get(); - Unreliables.retryUntilTrue( - 10, - TimeUnit.SECONDS, - () -> { + Awaitility + .await() + .atMost(Duration.ofSeconds(10)) + .untilAsserted(() -> { ConsumerRecords records = consumer.poll(Duration.ofMillis(100)); - if (records.isEmpty()) { - return false; - } - assertThat(records) .hasSize(1) .extracting(ConsumerRecord::topic, ConsumerRecord::key, ConsumerRecord::value) .containsExactly(tuple(topicName, "testcontainers", "rulezzz")); - - return true; - } - ); + }); consumer.unsubscribe(); } diff --git a/modules/redpanda/src/test/java/org/testcontainers/redpanda/CompatibleImageTest.java b/modules/redpanda/src/test/java/org/testcontainers/redpanda/CompatibleImageTest.java index cdb2af6a291..1167642615f 100644 --- a/modules/redpanda/src/test/java/org/testcontainers/redpanda/CompatibleImageTest.java +++ b/modules/redpanda/src/test/java/org/testcontainers/redpanda/CompatibleImageTest.java @@ -1,26 +1,18 @@ package org.testcontainers.redpanda; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; -@RunWith(Parameterized.class) -public class CompatibleImageTest extends AbstractRedpanda { +class CompatibleImageTest extends AbstractRedpanda { - private final String image; - - public CompatibleImageTest(String image) { - this.image = image; - } - - @Parameterized.Parameters(name = "{0}") public static String[] image() { - return new String[] { "docker.redpanda.com/vectorized/redpanda:v22.2.1", "redpandadata/redpanda:v22.2.1" }; + return new String[] { "docker.redpanda.com/redpandadata/redpanda:v22.2.1", "redpandadata/redpanda:v22.2.1" }; } - @Test - public void shouldProduceAndConsumeMessage() throws Exception { - try (RedpandaContainer container = new RedpandaContainer(this.image)) { + @ParameterizedTest + @MethodSource("image") + void shouldProduceAndConsumeMessage(String image) throws Exception { + try (RedpandaContainer container = new RedpandaContainer(image)) { container.start(); testKafkaFunctionality(container.getBootstrapServers()); } diff --git a/modules/redpanda/src/test/java/org/testcontainers/redpanda/RedpandaContainerTest.java b/modules/redpanda/src/test/java/org/testcontainers/redpanda/RedpandaContainerTest.java index d0df2d7c95b..6d1f35da0f2 100644 --- a/modules/redpanda/src/test/java/org/testcontainers/redpanda/RedpandaContainerTest.java +++ b/modules/redpanda/src/test/java/org/testcontainers/redpanda/RedpandaContainerTest.java @@ -12,9 +12,10 @@ import org.apache.kafka.common.errors.SaslAuthenticationException; import org.apache.kafka.common.errors.TopicAuthorizationException; import org.awaitility.Awaitility; -import org.junit.Test; +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; @@ -29,14 +30,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -public class RedpandaContainerTest extends AbstractRedpanda { +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 - public void testUsage() throws Exception { + void testUsage() throws Exception { try (RedpandaContainer container = new RedpandaContainer(REDPANDA_DOCKER_IMAGE)) { container.start(); testKafkaFunctionality(container.getBootstrapServers()); @@ -44,7 +45,7 @@ public void testUsage() throws Exception { } @Test - public void testUsageWithStringImage() throws Exception { + void testUsageWithStringImage() throws Exception { try ( // constructorWithVersion { RedpandaContainer container = new RedpandaContainer("docker.redpanda.com/redpandadata/redpanda:v23.1.2") @@ -60,28 +61,21 @@ public void testUsageWithStringImage() throws Exception { } @Test - public void testNotCompatibleVersion() { + 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 - public void vectorizedRedpandaImageVersion2221ShouldNotBeCompatible() { - assertThatThrownBy(() -> new RedpandaContainer("docker.redpanda.com/vectorized/redpanda:v21.11.19")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Redpanda version must be >= v22.2.1"); - } - - @Test - public void redpandadataRedpandaImageVersion2221ShouldNotBeCompatible() { + void redpandadataRedpandaImageVersion2221ShouldNotBeCompatible() { assertThatThrownBy(() -> new RedpandaContainer("redpandadata/redpanda:v21.11.19")) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Redpanda version must be >= v22.2.1"); } @Test - public void testSchemaRegistry() { + void testSchemaRegistry() { try (RedpandaContainer container = new RedpandaContainer(REDPANDA_DOCKER_IMAGE)) { container.start(); @@ -111,16 +105,43 @@ public void testSchemaRegistry() { } @Test - public void testUsageWithListener() throws Exception { + void testUsageWithListener() throws Exception { try ( Network network = Network.newNetwork(); - // registerListener { 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.4.1") + GenericContainer kcat = new GenericContainer<>("confluentinc/cp-kcat:7.9.0") .withCreateContainerCmdModifier(cmd -> { cmd.withEntrypoint("sh"); }) @@ -129,20 +150,44 @@ public void testUsageWithListener() throws Exception { .withCommand("-c", "tail -f /dev/null") // } ) { - redpanda.start(); + kafka.start(); kcat.start(); + // produceConsumeMessage { - kcat.execInContainer("kcat", "-b", "redpanda:19092", "-t", "msgs", "-P", "-l", "/data/msgs.txt"); + kcat.execInContainer("kcat", "-b", "kafka:19092", "-t", "msgs", "-P", "-l", "/data/msgs.txt"); String stdout = kcat - .execInContainer("kcat", "-b", "redpanda:19092", "-C", "-t", "msgs", "-c", "1") + .execInContainer("kcat", "-b", "kafka:19092", "-C", "-t", "msgs", "-c", "1") .getStdout(); // } + assertThat(stdout).contains("Message produced by kcat"); } } @Test - public void testUsageWithListenerAndSasl() throws Exception { + 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"; @@ -153,9 +198,9 @@ public void testUsageWithListenerAndSasl() throws Exception { .enableAuthorization() .enableSasl() .withSuperuser("panda") - .withListener(() -> "my-panda:29092") + .withListener("my-panda:29092") .withNetwork(network); - GenericContainer kcat = new GenericContainer<>("confluentinc/cp-kcat:7.4.1") + GenericContainer kcat = new GenericContainer<>("confluentinc/cp-kcat:7.9.0") .withCreateContainerCmdModifier(cmd -> { cmd.withEntrypoint("sh"); }) @@ -221,7 +266,7 @@ public void testUsageWithListenerAndSasl() throws Exception { @SneakyThrows @Test - public void enableSaslWithSuccessfulTopicCreation() { + void enableSaslWithSuccessfulTopicCreation() { try ( // security { RedpandaContainer redpanda = new RedpandaContainer("docker.redpanda.com/redpandadata/redpanda:v23.1.7") @@ -244,7 +289,7 @@ public void enableSaslWithSuccessfulTopicCreation() { } @Test - public void enableSaslWithUnsuccessfulTopicCreation() { + void enableSaslWithUnsuccessfulTopicCreation() { try ( RedpandaContainer redpanda = new RedpandaContainer("docker.redpanda.com/redpandadata/redpanda:v23.1.7") .enableAuthorization() @@ -268,7 +313,7 @@ public void enableSaslWithUnsuccessfulTopicCreation() { } @Test - public void enableSaslAndWithAuthenticationError() { + void enableSaslAndWithAuthenticationError() { try ( RedpandaContainer redpanda = new RedpandaContainer("docker.redpanda.com/redpandadata/redpanda:v23.1.7") .enableAuthorization() @@ -290,7 +335,7 @@ public void enableSaslAndWithAuthenticationError() { } @Test - public void schemaRegistryWithHttpBasic() { + void schemaRegistryWithHttpBasic() { try ( RedpandaContainer redpanda = new RedpandaContainer("docker.redpanda.com/redpandadata/redpanda:v23.1.7") .enableSchemaRegistryHttpBasicAuth() @@ -317,7 +362,7 @@ public void schemaRegistryWithHttpBasic() { @SneakyThrows @Test - public void testRestProxy() { + void testRestProxy() { try (RedpandaContainer redpanda = new RedpandaContainer("docker.redpanda.com/redpandadata/redpanda:v23.1.7")) { redpanda.start(); diff --git a/modules/scylladb/build.gradle b/modules/scylladb/build.gradle new file mode 100644 index 00000000000..480d1958609 --- /dev/null +++ b/modules/scylladb/build.gradle @@ -0,0 +1,8 @@ +description = "Testcontainers :: ScyllaDB" + +dependencies { + api project(":testcontainers") + + testImplementation 'com.scylladb:java-driver-core:4.19.0.8' + testImplementation 'software.amazon.awssdk:dynamodb:2.46.20' +} diff --git a/modules/scylladb/src/main/java/org/testcontainers/scylladb/ScyllaDBContainer.java b/modules/scylladb/src/main/java/org/testcontainers/scylladb/ScyllaDBContainer.java new file mode 100644 index 00000000000..f181b8b9f00 --- /dev/null +++ b/modules/scylladb/src/main/java/org/testcontainers/scylladb/ScyllaDBContainer.java @@ -0,0 +1,109 @@ +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); + } +} diff --git a/modules/scylladb/src/test/java/org/testcontainers/scylladb/ScyllaDBContainerTest.java b/modules/scylladb/src/test/java/org/testcontainers/scylladb/ScyllaDBContainerTest.java new file mode 100644 index 00000000000..d500aa74868 --- /dev/null +++ b/modules/scylladb/src/test/java/org/testcontainers/scylladb/ScyllaDBContainerTest.java @@ -0,0 +1,203 @@ +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"); + } + } +} diff --git a/modules/scylladb/src/test/resources/keys/node0.cer b/modules/scylladb/src/test/resources/keys/node0.cer new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/scylladb/src/test/resources/keys/node0.p12 b/modules/scylladb/src/test/resources/keys/node0.p12 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/modules/scylladb/src/test/resources/keys/scylla.cer.pem b/modules/scylladb/src/test/resources/keys/scylla.cer.pem new file mode 100644 index 00000000000..8f538448288 --- /dev/null +++ b/modules/scylladb/src/test/resources/keys/scylla.cer.pem @@ -0,0 +1,30 @@ +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----- diff --git a/modules/scylladb/src/test/resources/keys/scylla.key.pem b/modules/scylladb/src/test/resources/keys/scylla.key.pem new file mode 100644 index 00000000000..26ff1a8b80d --- /dev/null +++ b/modules/scylladb/src/test/resources/keys/scylla.key.pem @@ -0,0 +1,44 @@ +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----- diff --git a/modules/scylladb/src/test/resources/keys/scylla.keystore b/modules/scylladb/src/test/resources/keys/scylla.keystore new file mode 100644 index 00000000000..7f027beaf64 Binary files /dev/null and b/modules/scylladb/src/test/resources/keys/scylla.keystore differ diff --git a/modules/scylladb/src/test/resources/keys/scylla.truststore b/modules/scylladb/src/test/resources/keys/scylla.truststore new file mode 100644 index 00000000000..a798f8dcce4 Binary files /dev/null and b/modules/scylladb/src/test/resources/keys/scylla.truststore differ diff --git a/modules/scylladb/src/test/resources/logback-test.xml b/modules/scylladb/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..83ef7a1a3ef --- /dev/null +++ b/modules/scylladb/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + diff --git a/modules/scylladb/src/test/resources/scylla-test-ssl/scylla.yaml b/modules/scylladb/src/test/resources/scylla-test-ssl/scylla.yaml new file mode 100644 index 00000000000..7d79fabb70e --- /dev/null +++ b/modules/scylladb/src/test/resources/scylla-test-ssl/scylla.yaml @@ -0,0 +1,662 @@ +# 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 diff --git a/modules/selenium/build.gradle b/modules/selenium/build.gradle index af8d50cbb00..79b525b6185 100644 --- a/modules/selenium/build.gradle +++ b/modules/selenium/build.gradle @@ -12,8 +12,7 @@ dependencies { testImplementation 'org.seleniumhq.selenium:selenium-support' testImplementation 'org.mortbay.jetty:jetty:6.1.26' - testImplementation project(':nginx') - testImplementation 'org.assertj:assertj-core:3.25.1' + testImplementation project(':testcontainers-nginx') - compileOnly 'org.jetbrains:annotations:24.1.0' + compileOnly 'org.jetbrains:annotations:26.1.0' } diff --git a/modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java b/modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java index 8cdb6ca611c..53ee8ba5577 100644 --- a/modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java +++ b/modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java @@ -46,7 +46,10 @@ * {@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 { diff --git a/modules/selenium/src/main/java/org/testcontainers/containers/RecordingFileFactory.java b/modules/selenium/src/main/java/org/testcontainers/containers/RecordingFileFactory.java index 31946dac59e..9f3795f7cb8 100644 --- a/modules/selenium/src/main/java/org/testcontainers/containers/RecordingFileFactory.java +++ b/modules/selenium/src/main/java/org/testcontainers/containers/RecordingFileFactory.java @@ -1,20 +1,10 @@ package org.testcontainers.containers; -import org.junit.runner.Description; import org.testcontainers.containers.VncRecordingContainer.VncRecordingFormat; import java.io.File; public interface RecordingFileFactory { - @Deprecated - default File recordingFileForTest(File vncRecordingDirectory, Description description, boolean succeeded) { - return recordingFileForTest( - vncRecordingDirectory, - description.getTestClass().getSimpleName() + "-" + description.getMethodName(), - succeeded - ); - } - default File recordingFileForTest( File vncRecordingDirectory, String prefix, diff --git a/modules/selenium/src/main/java/org/testcontainers/selenium/BrowserWebDriverContainer.java b/modules/selenium/src/main/java/org/testcontainers/selenium/BrowserWebDriverContainer.java new file mode 100644 index 00000000000..97ac23f5d55 --- /dev/null +++ b/modules/selenium/src/main/java/org/testcontainers/selenium/BrowserWebDriverContainer.java @@ -0,0 +1,283 @@ +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, + } +} diff --git a/modules/selenium/src/test/java/org/testcontainers/SeleniumTestImages.java b/modules/selenium/src/test/java/org/testcontainers/SeleniumTestImages.java deleted file mode 100644 index 82d19adf3b5..00000000000 --- a/modules/selenium/src/test/java/org/testcontainers/SeleniumTestImages.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.testcontainers; - -import org.testcontainers.utility.DockerImageName; - -public interface SeleniumTestImages { - DockerImageName NGINX_TEST_IMAGE = DockerImageName.parse("nginx:1.9.4"); -} diff --git a/modules/selenium/src/test/java/org/testcontainers/containers/DefaultRecordingFileFactoryTest.java b/modules/selenium/src/test/java/org/testcontainers/containers/DefaultRecordingFileFactoryTest.java index aa894810930..03540e54b01 100644 --- a/modules/selenium/src/test/java/org/testcontainers/containers/DefaultRecordingFileFactoryTest.java +++ b/modules/selenium/src/test/java/org/testcontainers/containers/DefaultRecordingFileFactoryTest.java @@ -1,10 +1,10 @@ package org.testcontainers.containers; import lombok.Value; -import org.junit.Test; -import org.junit.runner.Description; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; +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; @@ -17,9 +17,10 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(Parameterized.class) +@ParameterizedClass +@MethodSource("data") @Value -public class DefaultRecordingFileFactoryTest { +class DefaultRecordingFileFactoryTest { private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("YYYYMMdd-HHmmss"); @@ -31,7 +32,6 @@ public class DefaultRecordingFileFactoryTest { private final boolean success; - @Parameterized.Parameters public static Collection data() { Collection args = new ArrayList<>(); args.add(new Object[] { "testMethod1", "FAILED", Boolean.FALSE }); @@ -40,13 +40,10 @@ public static Collection data() { } @Test - public void recordingFileThatShouldDescribeTheTestResultAtThePresentTime() throws Exception { + public void recordingFileThatShouldDescribeTheTestResultAtThePresentTime(TestInfo testInfo) throws Exception { File vncRecordingDirectory = Files.createTempDirectory("recording").toFile(); - Description description = Description.createTestDescription( - getClass().getCanonicalName(), - methodName, - Test.class - ); + String className = testInfo.getTestClass().orElseThrow(IllegalStateException::new).getSimpleName(); + String description = className + "-" + methodName; File recordingFile = factory.recordingFileForTest(vncRecordingDirectory, description, success); diff --git a/modules/selenium/src/test/java/org/testcontainers/junit/ChromeWebDriverContainerTest.java b/modules/selenium/src/test/java/org/testcontainers/junit/ChromeWebDriverContainerTest.java deleted file mode 100644 index 2f0bfc27b0d..00000000000 --- a/modules/selenium/src/test/java/org/testcontainers/junit/ChromeWebDriverContainerTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.testcontainers.junit; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.openqa.selenium.chrome.ChromeOptions; -import org.testcontainers.containers.BrowserWebDriverContainer; - -/** - * - */ -public class ChromeWebDriverContainerTest extends BaseWebDriverContainerTest { - - // junitRule { - @Rule - public BrowserWebDriverContainer chrome = new BrowserWebDriverContainer<>() - .withCapabilities(new ChromeOptions()) - // } - .withNetwork(NETWORK); - - @Before - public void checkBrowserIsIndeedChrome() { - assertBrowserNameIs(chrome, "chrome", new ChromeOptions()); - } - - @Test - public void simpleExploreTest() { - doSimpleExplore(chrome, new ChromeOptions()); - } -} diff --git a/modules/selenium/src/test/java/org/testcontainers/junit/ContainerWithoutCapabilitiesTest.java b/modules/selenium/src/test/java/org/testcontainers/junit/ContainerWithoutCapabilitiesTest.java deleted file mode 100644 index f5c03fb32d1..00000000000 --- a/modules/selenium/src/test/java/org/testcontainers/junit/ContainerWithoutCapabilitiesTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.testcontainers.junit; - -import org.junit.Rule; -import org.junit.Test; -import org.openqa.selenium.chrome.ChromeOptions; -import org.testcontainers.containers.BrowserWebDriverContainer; - -public class ContainerWithoutCapabilitiesTest extends BaseWebDriverContainerTest { - - @Rule - public BrowserWebDriverContainer chrome = new BrowserWebDriverContainer<>().withNetwork(NETWORK); - - @Test - public void chromeIsStartedIfNoCapabilitiesProvided() { - assertBrowserNameIs(chrome, "chrome", new ChromeOptions()); - } - - @Test - public void simpleExploreTestWhenNoCapabilitiesProvided() { - doSimpleExplore(chrome, new ChromeOptions()); - } -} diff --git a/modules/selenium/src/test/java/org/testcontainers/junit/CustomWaitTimeoutWebDriverContainerTest.java b/modules/selenium/src/test/java/org/testcontainers/junit/CustomWaitTimeoutWebDriverContainerTest.java deleted file mode 100644 index 43639010e59..00000000000 --- a/modules/selenium/src/test/java/org/testcontainers/junit/CustomWaitTimeoutWebDriverContainerTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.testcontainers.junit; - -import org.junit.Rule; -import org.junit.Test; -import org.openqa.selenium.chrome.ChromeOptions; -import org.testcontainers.containers.BrowserWebDriverContainer; - -import java.time.Duration; -import java.time.temporal.ChronoUnit; - -/** - * - */ -public class CustomWaitTimeoutWebDriverContainerTest extends BaseWebDriverContainerTest { - - @Rule - public BrowserWebDriverContainer chromeWithCustomTimeout = new BrowserWebDriverContainer<>() - .withCapabilities(new ChromeOptions()) - .withStartupTimeout(Duration.of(30, ChronoUnit.SECONDS)) - .withNetwork(NETWORK); - - @Test - public void simpleExploreTest() { - doSimpleExplore(chromeWithCustomTimeout, new ChromeOptions()); - } -} diff --git a/modules/selenium/src/test/java/org/testcontainers/junit/EdgeWebDriverContainerTest.java b/modules/selenium/src/test/java/org/testcontainers/junit/EdgeWebDriverContainerTest.java deleted file mode 100644 index f1935fa84ee..00000000000 --- a/modules/selenium/src/test/java/org/testcontainers/junit/EdgeWebDriverContainerTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.testcontainers.junit; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.openqa.selenium.edge.EdgeOptions; -import org.testcontainers.containers.BrowserWebDriverContainer; - -public class EdgeWebDriverContainerTest extends BaseWebDriverContainerTest { - - // junitRule { - @Rule - public BrowserWebDriverContainer edge = new BrowserWebDriverContainer<>() - .withCapabilities(new EdgeOptions()) - // } - .withNetwork(NETWORK); - - @Before - public void checkBrowserIsIndeedMSEdge() { - assertBrowserNameIs(edge, "msedge", new EdgeOptions()); - } - - @Test - public void simpleExploreTest() { - doSimpleExplore(edge, new EdgeOptions()); - } -} diff --git a/modules/selenium/src/test/java/org/testcontainers/junit/FirefoxWebDriverContainerTest.java b/modules/selenium/src/test/java/org/testcontainers/junit/FirefoxWebDriverContainerTest.java deleted file mode 100644 index 211800b8fad..00000000000 --- a/modules/selenium/src/test/java/org/testcontainers/junit/FirefoxWebDriverContainerTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.testcontainers.junit; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.openqa.selenium.firefox.FirefoxOptions; -import org.testcontainers.containers.BrowserWebDriverContainer; - -/** - * - */ -public class FirefoxWebDriverContainerTest extends BaseWebDriverContainerTest { - - // junitRule { - @Rule - public BrowserWebDriverContainer firefox = new BrowserWebDriverContainer<>() - .withCapabilities(new FirefoxOptions()) - // } - .withNetwork(NETWORK); - - @Before - public void checkBrowserIsIndeedFirefox() { - assertBrowserNameIs(firefox, "firefox", new FirefoxOptions()); - } - - @Test - public void simpleExploreTest() { - doSimpleExplore(firefox, new FirefoxOptions()); - } -} diff --git a/modules/selenium/src/test/java/org/testcontainers/junit/SeleniumStartTest.java b/modules/selenium/src/test/java/org/testcontainers/junit/SeleniumStartTest.java index 04a31662099..a1923b0264e 100644 --- a/modules/selenium/src/test/java/org/testcontainers/junit/SeleniumStartTest.java +++ b/modules/selenium/src/test/java/org/testcontainers/junit/SeleniumStartTest.java @@ -1,8 +1,9 @@ package org.testcontainers.junit; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; +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; @@ -10,19 +11,19 @@ /** * Simple test to check that readiness detection works correctly across major versions of the containers. */ -@RunWith(Parameterized.class) +@ParameterizedClass(name = "tag: {0}") +@MethodSource("data") public class SeleniumStartTest { - @Parameterized.Parameters(name = "tag: {0}") public static String[] data() { - return new String[] { "4.0.0", "3.4.0", "2.53.0", "2.45.0" }; + return new String[] { "4.0.0", "3.4.0", "2.53.0" }; } - @Parameterized.Parameter + @Parameter public String tag; @Test - public void testAdditionalStartupString() { + void testAdditionalStartupString() { final DockerImageName imageName = DockerImageName.parse("selenium/standalone-chrome").withTag(tag); try ( BrowserWebDriverContainer chrome = new BrowserWebDriverContainer<>(imageName) diff --git a/modules/selenium/src/test/java/org/testcontainers/junit/SeleniumUtilsTest.java b/modules/selenium/src/test/java/org/testcontainers/junit/SeleniumUtilsTest.java index 6208cb3a5bb..b9c2125f93b 100644 --- a/modules/selenium/src/test/java/org/testcontainers/junit/SeleniumUtilsTest.java +++ b/modules/selenium/src/test/java/org/testcontainers/junit/SeleniumUtilsTest.java @@ -1,6 +1,6 @@ package org.testcontainers.junit; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.SeleniumUtils; import java.io.IOException; @@ -11,15 +11,15 @@ /** * Created by Julien LAMY */ -public class SeleniumUtilsTest { +class SeleniumUtilsTest { @Test - public void detectSeleniumVersionUnder3() throws IOException { + void detectSeleniumVersionUnder3() throws IOException { checkSeleniumVersionDetected("manifests/MANIFEST-2.45.0.MF", "2.45.0"); } @Test - public void detectSeleniumVersionUpper3() throws IOException { + void detectSeleniumVersionUpper3() throws IOException { checkSeleniumVersionDetected("manifests/MANIFEST-3.5.2.MF", "3.5.2"); } diff --git a/modules/selenium/src/test/java/org/testcontainers/junit/SpecificImageNameWebDriverContainerTest.java b/modules/selenium/src/test/java/org/testcontainers/junit/SpecificImageNameWebDriverContainerTest.java deleted file mode 100644 index 9f0d9035f44..00000000000 --- a/modules/selenium/src/test/java/org/testcontainers/junit/SpecificImageNameWebDriverContainerTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.testcontainers.junit; - -import org.junit.Rule; -import org.junit.Test; -import org.openqa.selenium.firefox.FirefoxOptions; -import org.testcontainers.containers.BrowserWebDriverContainer; -import org.testcontainers.utility.DockerImageName; - -public class SpecificImageNameWebDriverContainerTest extends BaseWebDriverContainerTest { - - private static final DockerImageName FIREFOX_IMAGE = DockerImageName.parse("selenium/standalone-firefox:4.10.0"); - - @Rule - public BrowserWebDriverContainer firefox = new BrowserWebDriverContainer<>(FIREFOX_IMAGE) - .withCapabilities(new FirefoxOptions()) - .withNetwork(NETWORK); - - @Test - public void simpleExploreTest() { - doSimpleExplore(firefox, new FirefoxOptions()); - } -} diff --git a/modules/selenium/src/test/java/org/testcontainers/junit/BaseWebDriverContainerTest.java b/modules/selenium/src/test/java/org/testcontainers/selenium/BaseWebDriverContainerTest.java similarity index 84% rename from modules/selenium/src/test/java/org/testcontainers/junit/BaseWebDriverContainerTest.java rename to modules/selenium/src/test/java/org/testcontainers/selenium/BaseWebDriverContainerTest.java index 97124802de1..eb7f946b6fa 100644 --- a/modules/selenium/src/test/java/org/testcontainers/junit/BaseWebDriverContainerTest.java +++ b/modules/selenium/src/test/java/org/testcontainers/selenium/BaseWebDriverContainerTest.java @@ -1,11 +1,9 @@ -package org.testcontainers.junit; +package org.testcontainers.selenium; -import org.junit.ClassRule; 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.BrowserWebDriverContainer; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; @@ -15,15 +13,10 @@ import static org.assertj.core.api.Assertions.assertThat; -/** - * - */ public class BaseWebDriverContainerTest { - @ClassRule public static Network NETWORK = Network.newNetwork(); - @ClassRule public static GenericContainer HELLO_WORLD = new GenericContainer<>( DockerImageName.parse("testcontainers/helloworld:1.1.0") ) @@ -32,7 +25,11 @@ public class BaseWebDriverContainerTest { .withExposedPorts(8080, 8081) .waitingFor(new HttpWaitStrategy()); - protected static void doSimpleExplore(BrowserWebDriverContainer rule, Capabilities capabilities) { + 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()); @@ -48,11 +45,11 @@ protected static void doSimpleExplore(BrowserWebDriverContainer rule, Capabil } protected void assertBrowserNameIs( - BrowserWebDriverContainer rule, + BrowserWebDriverContainer container, String expectedName, Capabilities capabilities ) { - RemoteWebDriver driver = new RemoteWebDriver(rule.getSeleniumAddress(), 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); diff --git a/modules/selenium/src/test/java/org/testcontainers/junit/BrowserWebDriverContainerTest.java b/modules/selenium/src/test/java/org/testcontainers/selenium/BrowserWebDriverContainerTest.java similarity index 71% rename from modules/selenium/src/test/java/org/testcontainers/junit/BrowserWebDriverContainerTest.java rename to modules/selenium/src/test/java/org/testcontainers/selenium/BrowserWebDriverContainerTest.java index 370d9cffedb..b5e632cd217 100644 --- a/modules/selenium/src/test/java/org/testcontainers/junit/BrowserWebDriverContainerTest.java +++ b/modules/selenium/src/test/java/org/testcontainers/selenium/BrowserWebDriverContainerTest.java @@ -1,31 +1,29 @@ -package org.testcontainers.junit; +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.Assume; -import org.junit.Test; -import org.openqa.selenium.chrome.ChromeOptions; -import org.openqa.selenium.firefox.FirefoxOptions; -import org.testcontainers.containers.BrowserWebDriverContainer; +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; -public class BrowserWebDriverContainerTest { +class BrowserWebDriverContainerTest { private static final String NO_PROXY_KEY = "no_proxy"; private static final String NO_PROXY_VALUE = "localhost,.noproxy-domain.com"; @Test - public void honorPresetNoProxyEnvironment() { + void honorPresetNoProxyEnvironment() { try ( - BrowserWebDriverContainer chromeWithNoProxySet = (BrowserWebDriverContainer) new BrowserWebDriverContainer() - .withCapabilities(new ChromeOptions()) + BrowserWebDriverContainer chromeWithNoProxySet = new BrowserWebDriverContainer( + "selenium/standalone-chrome:4.13.0" + ) .withEnv(NO_PROXY_KEY, NO_PROXY_VALUE) ) { chromeWithNoProxySet.start(); @@ -36,10 +34,11 @@ public void honorPresetNoProxyEnvironment() { } @Test - public void provideDefaultNoProxyEnvironmentIfNotSet() { + void provideDefaultNoProxyEnvironmentIfNotSet() { try ( - BrowserWebDriverContainer chromeWithoutNoProxySet = new BrowserWebDriverContainer() - .withCapabilities(new ChromeOptions()) + BrowserWebDriverContainer chromeWithoutNoProxySet = new BrowserWebDriverContainer( + "selenium/standalone-chrome:4.13.0" + ) ) { chromeWithoutNoProxySet.start(); @@ -49,11 +48,12 @@ public void provideDefaultNoProxyEnvironmentIfNotSet() { } @Test - public void createContainerWithShmVolume() { - Assume.assumeFalse("SHM isn't mounted on Windows", SystemUtils.IS_OS_WINDOWS); + void createContainerWithShmVolume() { + assumeThat(SystemUtils.IS_OS_WINDOWS).isTrue(); try ( - BrowserWebDriverContainer webDriverContainer = new BrowserWebDriverContainer() - .withCapabilities(new FirefoxOptions()) + BrowserWebDriverContainer webDriverContainer = new BrowserWebDriverContainer( + "selenium/standalone-firefox:4.13.0" + ) ) { webDriverContainer.start(); @@ -66,11 +66,12 @@ public void createContainerWithShmVolume() { } @Test - public void createContainerWithoutShmVolume() { + void createContainerWithoutShmVolume() { try ( - BrowserWebDriverContainer webDriverContainer = new BrowserWebDriverContainer<>() + BrowserWebDriverContainer webDriverContainer = new BrowserWebDriverContainer( + "selenium/standalone-firefox:4.13.0" + ) .withSharedMemorySize(512 * FileUtils.ONE_MB) - .withCapabilities(new FirefoxOptions()) ) { webDriverContainer.start(); diff --git a/modules/selenium/src/test/java/org/testcontainers/junit/ChromeRecordingWebDriverContainerTest.java b/modules/selenium/src/test/java/org/testcontainers/selenium/ChromeRecordingWebDriverContainerTest.java similarity index 71% rename from modules/selenium/src/test/java/org/testcontainers/junit/ChromeRecordingWebDriverContainerTest.java rename to modules/selenium/src/test/java/org/testcontainers/selenium/ChromeRecordingWebDriverContainerTest.java index 8723d951d3e..dbb39b86599 100644 --- a/modules/selenium/src/test/java/org/testcontainers/junit/ChromeRecordingWebDriverContainerTest.java +++ b/modules/selenium/src/test/java/org/testcontainers/selenium/ChromeRecordingWebDriverContainerTest.java @@ -1,25 +1,23 @@ -package org.testcontainers.junit; +package org.testcontainers.selenium; import com.google.common.io.PatternFilenameFilter; -import org.junit.Rule; -import org.junit.Test; -import org.junit.experimental.runners.Enclosed; -import org.junit.rules.TemporaryFolder; -import org.junit.runner.RunWith; +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.BrowserWebDriverContainer; -import org.testcontainers.containers.BrowserWebDriverContainer.VncRecordingMode; 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; @@ -27,8 +25,7 @@ import static org.assertj.core.api.Assertions.assertThat; -@RunWith(Enclosed.class) -public class ChromeRecordingWebDriverContainerTest extends BaseWebDriverContainerTest { +class ChromeRecordingWebDriverContainerTest extends BaseWebDriverContainerTest { /** * Guaranty a minimum video length for FFmpeg re-encoding. @@ -36,19 +33,19 @@ public class ChromeRecordingWebDriverContainerTest extends BaseWebDriverContaine */ private static final int MINIMUM_VIDEO_DURATION_MILLISECONDS = 200; - public static class ChromeThatRecordsAllTests { + @Nested + class ChromeThatRecordsAllTests { - @Rule - public TemporaryFolder vncRecordingDirectory = new TemporaryFolder(); + @TempDir + public Path vncRecordingDirectory; @Test - public void recordingTestThatShouldBeRecordedAndRetainedInFlvFormatAsDefault() throws InterruptedException { - File target = vncRecordingDirectory.getRoot(); + 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<>() - .withCapabilities(new ChromeOptions()) + BrowserWebDriverContainer chrome = new BrowserWebDriverContainer("selenium/standalone-chrome:4.13.0") .withRecordingMode(VncRecordingMode.RECORD_ALL, target) // } .withRecordingFileFactory(new DefaultRecordingFileFactory()) @@ -59,7 +56,7 @@ public void recordingTestThatShouldBeRecordedAndRetainedInFlvFormatAsDefault() t } } - private File[] runSimpleExploreInContainer(BrowserWebDriverContainer container, String fileNamePattern) + private File[] runSimpleExploreInContainer(BrowserWebDriverContainer container, String fileNamePattern) throws InterruptedException { container.start(); @@ -80,17 +77,16 @@ public String getFilesystemFriendlyName() { Optional.empty() ); - return vncRecordingDirectory.getRoot().listFiles(new PatternFilenameFilter(fileNamePattern)); + return vncRecordingDirectory.toFile().listFiles(new PatternFilenameFilter(fileNamePattern)); } @Test - public void recordingTestShouldHaveFlvExtension() throws InterruptedException { - File target = vncRecordingDirectory.getRoot(); + void recordingTestShouldHaveFlvExtension() throws InterruptedException { + File target = vncRecordingDirectory.toFile(); try ( // recordFlv { // Set (explicitly) FLV format for recorded video: - BrowserWebDriverContainer chrome = new BrowserWebDriverContainer<>() - .withCapabilities(new ChromeOptions()) + BrowserWebDriverContainer chrome = new BrowserWebDriverContainer("selenium/standalone-chrome:4.13.0") .withRecordingMode(VncRecordingMode.RECORD_ALL, target, VncRecordingFormat.FLV) // } .withRecordingFileFactory(new DefaultRecordingFileFactory()) @@ -102,13 +98,12 @@ public void recordingTestShouldHaveFlvExtension() throws InterruptedException { } @Test - public void recordingTestShouldHaveMp4Extension() throws InterruptedException { - File target = vncRecordingDirectory.getRoot(); + void recordingTestShouldHaveMp4Extension() throws InterruptedException { + File target = vncRecordingDirectory.toFile(); try ( // recordMp4 { // Set MP4 format for recorded video: - BrowserWebDriverContainer chrome = new BrowserWebDriverContainer<>() - .withCapabilities(new ChromeOptions()) + BrowserWebDriverContainer chrome = new BrowserWebDriverContainer("selenium/standalone-chrome:4.13.0") .withRecordingMode(VncRecordingMode.RECORD_ALL, target, VncRecordingFormat.MP4) // } .withRecordingFileFactory(new DefaultRecordingFileFactory()) @@ -120,12 +115,11 @@ public void recordingTestShouldHaveMp4Extension() throws InterruptedException { } @Test - public void recordingTestThatShouldHaveCorrectDuration() throws IOException, InterruptedException { + void recordingTestThatShouldHaveCorrectDuration() throws IOException, InterruptedException { MountableFile mountableFile; try ( - BrowserWebDriverContainer chrome = new BrowserWebDriverContainer<>() - .withCapabilities(new ChromeOptions()) - .withRecordingMode(VncRecordingMode.RECORD_ALL, vncRecordingDirectory.getRoot()) + BrowserWebDriverContainer chrome = new BrowserWebDriverContainer("selenium/standalone-chrome:4.13.0") + .withRecordingMode(VncRecordingMode.RECORD_ALL, vncRecordingDirectory.toFile()) .withRecordingFileFactory(new DefaultRecordingFileFactory()) .withNetwork(NETWORK) ) { @@ -159,18 +153,18 @@ public void recordingTestThatShouldHaveCorrectDuration() throws IOException, Int } } - public static class ChromeThatRecordsFailingTests { + @Nested + class ChromeThatRecordsFailingTests { - @Rule - public TemporaryFolder vncRecordingDirectory = new TemporaryFolder(); + @TempDir + public Path vncRecordingDirectory; @Test - public void recordingTestThatShouldBeRecordedButNotPersisted() { + void recordingTestThatShouldBeRecordedButNotPersisted() { try ( // withRecordingFileFactory { - BrowserWebDriverContainer chrome = new BrowserWebDriverContainer<>() + BrowserWebDriverContainer chrome = new BrowserWebDriverContainer("selenium/standalone-chrome:4.13.0") // } - .withCapabilities(new ChromeOptions()) // withRecordingFileFactory { .withRecordingFileFactory(new CustomRecordingFileFactory()) // } @@ -183,13 +177,12 @@ public void recordingTestThatShouldBeRecordedButNotPersisted() { } @Test - public void recordingTestThatShouldBeRecordedAndRetained() throws InterruptedException { - File target = vncRecordingDirectory.getRoot(); + void recordingTestThatShouldBeRecordedAndRetained() throws InterruptedException { + File target = vncRecordingDirectory.toFile(); try ( // recordFailing { // or if you only want videos for test failures: - BrowserWebDriverContainer chrome = new BrowserWebDriverContainer<>() - .withCapabilities(new ChromeOptions()) + BrowserWebDriverContainer chrome = new BrowserWebDriverContainer("selenium/standalone-chrome:4.13.0") .withRecordingMode(VncRecordingMode.RECORD_FAILING, target) // } .withRecordingFileFactory(new DefaultRecordingFileFactory()) @@ -214,11 +207,11 @@ public String getFilesystemFriendlyName() { Optional.of(new RuntimeException("Force writing of video file.")) ); - String[] files = vncRecordingDirectory.getRoot().list(new PatternFilenameFilter("FAILED-.*\\.flv")); + String[] files = vncRecordingDirectory.toFile().list(new PatternFilenameFilter("FAILED-.*\\.flv")); assertThat(files).as("recorded file count").hasSize(1); } } - private static class CustomRecordingFileFactory extends DefaultRecordingFileFactory {} + class CustomRecordingFileFactory extends DefaultRecordingFileFactory {} } } diff --git a/modules/selenium/src/test/java/org/testcontainers/selenium/ChromeWebDriverContainerTest.java b/modules/selenium/src/test/java/org/testcontainers/selenium/ChromeWebDriverContainerTest.java new file mode 100644 index 00000000000..85f0378f84a --- /dev/null +++ b/modules/selenium/src/test/java/org/testcontainers/selenium/ChromeWebDriverContainerTest.java @@ -0,0 +1,24 @@ +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()); + } +} diff --git a/modules/selenium/src/test/java/org/testcontainers/selenium/ContainerWithoutCapabilitiesTest.java b/modules/selenium/src/test/java/org/testcontainers/selenium/ContainerWithoutCapabilitiesTest.java new file mode 100644 index 00000000000..c706a0ce0ef --- /dev/null +++ b/modules/selenium/src/test/java/org/testcontainers/selenium/ContainerWithoutCapabilitiesTest.java @@ -0,0 +1,28 @@ +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()); + } +} diff --git a/modules/selenium/src/test/java/org/testcontainers/selenium/CustomWaitTimeoutWebDriverContainerTest.java b/modules/selenium/src/test/java/org/testcontainers/selenium/CustomWaitTimeoutWebDriverContainerTest.java new file mode 100644 index 00000000000..61d14bab65f --- /dev/null +++ b/modules/selenium/src/test/java/org/testcontainers/selenium/CustomWaitTimeoutWebDriverContainerTest.java @@ -0,0 +1,22 @@ +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()); + } +} diff --git a/modules/selenium/src/test/java/org/testcontainers/selenium/EdgeWebDriverContainerTest.java b/modules/selenium/src/test/java/org/testcontainers/selenium/EdgeWebDriverContainerTest.java new file mode 100644 index 00000000000..d99df91f8e1 --- /dev/null +++ b/modules/selenium/src/test/java/org/testcontainers/selenium/EdgeWebDriverContainerTest.java @@ -0,0 +1,24 @@ +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()); + } +} diff --git a/modules/selenium/src/test/java/org/testcontainers/selenium/FirefoxWebDriverContainerTest.java b/modules/selenium/src/test/java/org/testcontainers/selenium/FirefoxWebDriverContainerTest.java new file mode 100644 index 00000000000..50545168260 --- /dev/null +++ b/modules/selenium/src/test/java/org/testcontainers/selenium/FirefoxWebDriverContainerTest.java @@ -0,0 +1,24 @@ +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()); + } +} diff --git a/modules/selenium/src/test/java/org/testcontainers/junit/LocalServerWebDriverContainerTest.java b/modules/selenium/src/test/java/org/testcontainers/selenium/LocalServerWebDriverContainerTest.java similarity index 79% rename from modules/selenium/src/test/java/org/testcontainers/junit/LocalServerWebDriverContainerTest.java rename to modules/selenium/src/test/java/org/testcontainers/selenium/LocalServerWebDriverContainerTest.java index 8d29cb57310..e5819b11786 100644 --- a/modules/selenium/src/test/java/org/testcontainers/junit/LocalServerWebDriverContainerTest.java +++ b/modules/selenium/src/test/java/org/testcontainers/selenium/LocalServerWebDriverContainerTest.java @@ -1,8 +1,7 @@ -package org.testcontainers.junit; +package org.testcontainers.selenium; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; +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; @@ -10,7 +9,6 @@ import org.openqa.selenium.chrome.ChromeOptions; import org.openqa.selenium.remote.RemoteWebDriver; import org.testcontainers.Testcontainers; -import org.testcontainers.containers.BrowserWebDriverContainer; import static org.assertj.core.api.Assertions.assertThat; @@ -18,17 +16,16 @@ * 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) */ -public class LocalServerWebDriverContainerTest { +class LocalServerWebDriverContainerTest { - @Rule - public BrowserWebDriverContainer chrome = new BrowserWebDriverContainer<>() - .withAccessToHost(true) - .withCapabilities(new ChromeOptions()); + public BrowserWebDriverContainer chrome = new BrowserWebDriverContainer("selenium/standalone-chrome:4.13.0") + .withAccessToHost(true); private int localPort; - @Before + @BeforeEach public void setupLocalServer() throws Exception { + chrome.start(); // Set up a local Jetty HTTP server Server server = new Server(); server.addConnector(new SocketConnector()); @@ -42,7 +39,7 @@ public void setupLocalServer() throws Exception { } @Test - public void testConnection() { + void testConnection() { // getWebDriver { RemoteWebDriver driver = new RemoteWebDriver(chrome.getSeleniumAddress(), new ChromeOptions()); // } diff --git a/modules/selenium/src/test/java/org/testcontainers/selenium/SpecificImageNameWebDriverContainerTest.java b/modules/selenium/src/test/java/org/testcontainers/selenium/SpecificImageNameWebDriverContainerTest.java new file mode 100644 index 00000000000..c01dc5ae97f --- /dev/null +++ b/modules/selenium/src/test/java/org/testcontainers/selenium/SpecificImageNameWebDriverContainerTest.java @@ -0,0 +1,18 @@ +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()); + } +} diff --git a/modules/solace/build.gradle b/modules/solace/build.gradle index c4a231c4aec..1a6d2ed5a65 100644 --- a/modules/solace/build.gradle +++ b/modules/solace/build.gradle @@ -3,12 +3,10 @@ description = "Testcontainers :: Solace" dependencies { api project(':testcontainers') - shaded 'org.awaitility:awaitility:4.2.0' + shaded 'org.awaitility:awaitility:4.3.0' - testImplementation 'org.assertj:assertj-core:3.25.1' - testImplementation 'com.solacesystems:sol-jcsmp:10.22.0' + testImplementation 'com.solacesystems:sol-jcsmp:10.30.1' 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' - } diff --git a/modules/solace/src/main/java/org/testcontainers/solace/Service.java b/modules/solace/src/main/java/org/testcontainers/solace/Service.java index de9366a0e31..9c9342ef789 100644 --- a/modules/solace/src/main/java/org/testcontainers/solace/Service.java +++ b/modules/solace/src/main/java/org/testcontainers/solace/Service.java @@ -4,15 +4,33 @@ * 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) { diff --git a/modules/solace/src/main/java/org/testcontainers/solace/SolaceContainer.java b/modules/solace/src/main/java/org/testcontainers/solace/SolaceContainer.java index 64b4365081f..2ab22c8ffa6 100644 --- a/modules/solace/src/main/java/org/testcontainers/solace/SolaceContainer.java +++ b/modules/solace/src/main/java/org/testcontainers/solace/SolaceContainer.java @@ -65,13 +65,21 @@ 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, 6592L) }); + cmd + .getHostConfig() + .withShmSize(SHM_SIZE) + .withUlimits(new Ulimit[] { new Ulimit("nofile", 2448L, 1048576L) }); }); - this.waitStrategy = Wait.forLogMessage(SOLACE_READY_MESSAGE, 1).withStartupTimeout(Duration.ofSeconds(60)); + waitingFor(Wait.forLogMessage(SOLACE_READY_MESSAGE, 1).withStartupTimeout(Duration.ofSeconds(60))); withExposedPorts(8080); withEnv("username_admin_globalaccesslevel", "admin"); withEnv("username_admin_password", "admin"); @@ -103,6 +111,17 @@ private Transferable createConfigurationScript() { 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 @@ -260,7 +279,7 @@ public SolaceContainer withVpn(String vpn) { * Sets the solace server ceritificates * * @param certFile Server certificate - * @param caFile Certified Authority ceritificate + * @param caFile Certified Authority certificate * @return This container. */ public SolaceContainer withClientCert(final MountableFile certFile, final MountableFile caFile) { diff --git a/modules/solace/src/test/java/org/testcontainers/solace/SolaceContainerAMQPTest.java b/modules/solace/src/test/java/org/testcontainers/solace/SolaceContainerAMQPTest.java index 6c68d27d358..e120534559b 100644 --- a/modules/solace/src/test/java/org/testcontainers/solace/SolaceContainerAMQPTest.java +++ b/modules/solace/src/test/java/org/testcontainers/solace/SolaceContainerAMQPTest.java @@ -1,8 +1,7 @@ package org.testcontainers.solace; import org.apache.qpid.jms.JmsConnectionFactory; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,8 +18,9 @@ import javax.jms.Topic; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; -public class SolaceContainerAMQPTest { +class SolaceContainerAMQPTest { private static final Logger LOGGER = LoggerFactory.getLogger(SolaceContainerAMQPTest.class); @@ -29,9 +29,9 @@ public class SolaceContainerAMQPTest { private static final String TOPIC_NAME = "Topic/ActualTopic"; @Test - public void testSolaceContainer() throws JMSException { + void testSolaceContainer() throws JMSException { try ( - SolaceContainer solaceContainer = new SolaceContainer("solace/solace-pubsub-standard:10.2") + SolaceContainer solaceContainer = new SolaceContainer("solace/solace-pubsub-standard:10.25.0") .withTopic(TOPIC_NAME, Service.AMQP) .withVpn("amqp-vpn") ) { @@ -57,7 +57,7 @@ private static Session createSession(String username, String password, String ho connection.start(); return session; } catch (Exception e) { - Assert.fail("Error connecting and setting up session! " + e.getMessage()); + fail("Error connecting and setting up session! " + e.getMessage()); return null; } } diff --git a/modules/solace/src/test/java/org/testcontainers/solace/SolaceContainerMQTTTest.java b/modules/solace/src/test/java/org/testcontainers/solace/SolaceContainerMQTTTest.java index ab7d5ed56bb..652f216f6d2 100644 --- a/modules/solace/src/test/java/org/testcontainers/solace/SolaceContainerMQTTTest.java +++ b/modules/solace/src/test/java/org/testcontainers/solace/SolaceContainerMQTTTest.java @@ -6,8 +6,7 @@ import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,8 +14,9 @@ import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; -public class SolaceContainerMQTTTest { +class SolaceContainerMQTTTest { private static final Logger LOGGER = LoggerFactory.getLogger(SolaceContainerMQTTTest.class); @@ -25,9 +25,9 @@ public class SolaceContainerMQTTTest { private static final String TOPIC_NAME = "Topic/ActualTopic"; @Test - public void testSolaceContainer() { + void testSolaceContainer() { try ( - SolaceContainer solaceContainer = new SolaceContainer("solace/solace-pubsub-standard:10.2") + SolaceContainer solaceContainer = new SolaceContainer("solace/solace-pubsub-standard:10.25.0") .withTopic(TOPIC_NAME, Service.MQTT) .withVpn("mqtt-vpn") ) { @@ -52,7 +52,7 @@ private static MqttClient createClient(String username, String password, String mqttClient.connect(connOpts); return mqttClient; } catch (Exception e) { - Assert.fail("Error connecting and setting up session! " + e.getMessage()); + fail("Error connecting and setting up session! " + e.getMessage()); return null; } } diff --git a/modules/solace/src/test/java/org/testcontainers/solace/SolaceContainerRESTTest.java b/modules/solace/src/test/java/org/testcontainers/solace/SolaceContainerRESTTest.java index f2722b986ca..3d6914027c1 100644 --- a/modules/solace/src/test/java/org/testcontainers/solace/SolaceContainerRESTTest.java +++ b/modules/solace/src/test/java/org/testcontainers/solace/SolaceContainerRESTTest.java @@ -12,23 +12,23 @@ import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; -import org.junit.Assert; -import org.junit.Test; +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; -public class SolaceContainerRESTTest { +class SolaceContainerRESTTest { private static final String MESSAGE = "HelloWorld"; private static final String TOPIC_NAME = "Topic/ActualTopic"; @Test - public void testSolaceContainer() throws IOException { + void testSolaceContainer() throws IOException { try ( - SolaceContainer solaceContainer = new SolaceContainer("solace/solace-pubsub-standard:10.2") + SolaceContainer solaceContainer = new SolaceContainer("solace/solace-pubsub-standard:10.25.0") .withTopic(TOPIC_NAME, Service.REST) .withVpn("rest-vpn") ) { @@ -44,7 +44,7 @@ private void testPublishMessageToSolace(SolaceContainer solaceContainer, Service request.addHeader(HttpHeaders.CONTENT_TYPE, "text/plain"); HttpResponse response = client.execute(request); if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { - Assert.fail("Cannot send message to solace - " + EntityUtils.toString(response.getEntity())); + fail("Cannot send message to solace - " + EntityUtils.toString(response.getEntity())); } assertThat(EntityUtils.toString(response.getEntity())).isEmpty(); } diff --git a/modules/solace/src/test/java/org/testcontainers/solace/SolaceContainerSMFTest.java b/modules/solace/src/test/java/org/testcontainers/solace/SolaceContainerSMFTest.java index d7ccd94998e..f4d73ea08f0 100644 --- a/modules/solace/src/test/java/org/testcontainers/solace/SolaceContainerSMFTest.java +++ b/modules/solace/src/test/java/org/testcontainers/solace/SolaceContainerSMFTest.java @@ -1,18 +1,20 @@ 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.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.utility.MountableFile; @@ -21,8 +23,9 @@ import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; -public class SolaceContainerSMFTest { +class SolaceContainerSMFTest { private static final Logger LOGGER = LoggerFactory.getLogger(SolaceContainerSMFTest.class); @@ -30,40 +33,75 @@ public class SolaceContainerSMFTest { private static final Topic TOPIC = JCSMPFactory.onlyInstance().createTopic("Topic/ActualTopic"); + private static final Queue QUEUE = JCSMPFactory.onlyInstance().createQueue("Queue"); + @Test - public void testSolaceContainerWithSimpleAuthentication() { + void testSolaceContainerWithSimpleAuthentication() { try ( // solaceContainerSetup { - SolaceContainer solaceContainer = new SolaceContainer("solace/solace-pubsub-standard:10.2") + SolaceContainer solaceContainer = new SolaceContainer("solace/solace-pubsub-standard:10.25.0") .withCredentials("user", "pass") - .withTopic("Topic/ActualTopic", Service.SMF) + .withTopic(TOPIC.getName(), Service.SMF) .withVpn("test_vpn") // } ) { solaceContainer.start(); JCSMPSession session = createSessionWithBasicAuth(solaceContainer); assertThat(session).isNotNull(); - assertThat(consumeMessageFromSolace(session)).isEqualTo(MESSAGE); + consumeMessageFromTopics(session); session.closeSession(); } } @Test - public void testSolaceContainerWithCertificates() { + 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.6") + SolaceContainer solaceContainer = new SolaceContainer("solace/solace-pubsub-standard:10.25.0") .withClientCert( MountableFile.forClasspathResource("solace.pem"), MountableFile.forClasspathResource("rootCA.crt") ) - .withTopic("Topic/ActualTopic", Service.SMF_SSL) + .withTopic(TOPIC.getName(), Service.SMF_SSL) // } ) { solaceContainer.start(); JCSMPSession session = createSessionWithCertificates(solaceContainer); assertThat(session).isNotNull(); - assertThat(consumeMessageFromSolace(session)).isEqualTo(MESSAGE); + consumeMessageFromTopics(session); session.closeSession(); } } @@ -107,12 +145,12 @@ private static JCSMPSession createSession(JCSMPProperties properties) { session.connect(); return session; } catch (Exception e) { - Assert.fail("Error connecting and setting up session! " + e.getMessage()); + fail("Error connecting and setting up session! " + e.getMessage()); return null; } } - private void publishMessageToSolace(JCSMPSession session) throws JCSMPException { + private static void publishMessageToSolaceTopic(JCSMPSession session) throws JCSMPException { XMLMessageProducer producer = session.getMessageProducer( new JCSMPStreamingPublishCorrelatingEventHandler() { @Override @@ -131,37 +169,49 @@ public void handleErrorEx(Object o, JCSMPException e, long l) { producer.send(msg, TOPIC); } - private String consumeMessageFromSolace(JCSMPSession session) { - CountDownLatch latch = new CountDownLatch(1); + private static void consumeMessageFromTopics(JCSMPSession session) { try { - String[] result = new String[1]; - XMLMessageConsumer cons = session.getMessageConsumer( - new XMLMessageListener() { - @Override - public void onReceive(BytesXMLMessage msg) { - if (msg instanceof TextMessage) { - TextMessage textMessage = (TextMessage) msg; - String message = textMessage.getText(); - result[0] = message; - LOGGER.info("TextMessage received: " + message); - } - latch.countDown(); - } - - @Override - public void onException(JCSMPException e) { - LOGGER.error("Exception received: " + e.getMessage()); - latch.countDown(); - } - } - ); + TestConsumer listener = new TestConsumer(); + XMLMessageConsumer cons = session.getMessageConsumer(listener); session.addSubscription(TOPIC); cons.start(); - publishMessageToSolace(session); - assertThat(latch.await(10L, TimeUnit.SECONDS)).isTrue(); - return result[0]; + publishMessageToSolaceTopic(session); + listener.waitForMessage(); } catch (Exception e) { - throw new RuntimeException("Cannot receive message from solace", 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); + } } } } diff --git a/modules/solr/build.gradle b/modules/solr/build.gradle index d4305686141..509bce7c66f 100644 --- a/modules/solr/build.gradle +++ b/modules/solr/build.gradle @@ -3,8 +3,7 @@ description = "Testcontainers :: Solr" dependencies { api project(':testcontainers') // TODO use JDK's HTTP client and/or Apache HttpClient5 - shaded 'com.squareup.okhttp3:okhttp:4.12.0' + shaded 'com.squareup.okhttp3:okhttp:5.4.0' - testImplementation 'org.apache.solr:solr-solrj:8.11.2' - testImplementation 'org.assertj:assertj-core:3.25.1' + testImplementation 'org.apache.solr:solr-solrj:8.11.4' } diff --git a/modules/solr/src/main/java/org/testcontainers/containers/SolrContainer.java b/modules/solr/src/main/java/org/testcontainers/containers/SolrContainer.java index 691950f61dc..a921eb2f458 100644 --- a/modules/solr/src/main/java/org/testcontainers/containers/SolrContainer.java +++ b/modules/solr/src/main/java/org/testcontainers/containers/SolrContainer.java @@ -4,6 +4,7 @@ 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; @@ -22,14 +23,13 @@ *

  • 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 IMAGE = DEFAULT_IMAGE_NAME.getUnversionedPart(); - @Deprecated public static final String DEFAULT_TAG = "8.3.0"; @@ -39,6 +39,8 @@ public class SolrContainer extends GenericContainer { private SolrContainerConfiguration configuration; + private final ComparableVersion imageVersion; + /** * @deprecated use {@link #SolrContainer(DockerImageName)} instead */ @@ -47,9 +49,6 @@ public SolrContainer() { this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); } - /** - * @deprecated use {@link #SolrContainer(DockerImageName)} instead - */ public SolrContainer(final String dockerImageName) { this(DockerImageName.parse(dockerImageName)); } @@ -63,6 +62,7 @@ public SolrContainer(final DockerImageName dockerImageName) { .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) { @@ -104,17 +104,21 @@ public int getZookeeperPort() { @SneakyThrows protected void configure() { if (configuration.getSolrSchema() != null && configuration.getSolrConfiguration() == null) { - throw new IllegalStateException("Solr needs to have a configuration is you want to use a schema"); + throw new IllegalStateException("Solr needs to have a configuration if you want to use a schema"); } // Generate Command Builder - String command = "solr -f"; + String command = "solr start -f"; // Add Default Ports this.addExposedPort(SOLR_PORT); // Configure Zookeeper if (configuration.isZookeeper()) { this.addExposedPort(ZOOKEEPER_PORT); - command = "-DzkRun -h localhost"; + if (this.imageVersion.isGreaterThanOrEqualTo("9.7.0")) { + command = "-DzkRun --host localhost"; + } else { + command = "-DzkRun -h localhost"; + } } // Apply generated Command @@ -135,7 +139,7 @@ protected void waitUntilContainerStarted() { @SneakyThrows protected void containerIsStarted(InspectContainerResponse containerInfo) { if (!configuration.isZookeeper()) { - ExecResult result = execInContainer("solr", "create_core", "-c", configuration.getCollectionName()); + 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() diff --git a/modules/solr/src/main/java/org/testcontainers/solr/SolrContainer.java b/modules/solr/src/main/java/org/testcontainers/solr/SolrContainer.java new file mode 100644 index 00000000000..536afa69dfd --- /dev/null +++ b/modules/solr/src/main/java/org/testcontainers/solr/SolrContainer.java @@ -0,0 +1,156 @@ +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() + ); + } +} diff --git a/modules/solr/src/test/java/org/testcontainers/containers/SolrContainerTest.java b/modules/solr/src/test/java/org/testcontainers/solr/SolrContainerTest.java similarity index 63% rename from modules/solr/src/test/java/org/testcontainers/containers/SolrContainerTest.java rename to modules/solr/src/test/java/org/testcontainers/solr/SolrContainerTest.java index 6bb4fabb1a4..f63c4d38164 100644 --- a/modules/solr/src/test/java/org/testcontainers/containers/SolrContainerTest.java +++ b/modules/solr/src/test/java/org/testcontainers/solr/SolrContainerTest.java @@ -1,34 +1,37 @@ -package org.testcontainers.containers; +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.After; -import org.junit.Test; -import org.testcontainers.utility.DockerImageName; +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; -public class SolrContainerTest { - - private static final DockerImageName SOLR_IMAGE = DockerImageName.parse("solr:8.3.0"); +class SolrContainerTest { private SolrClient client = null; - @After - public void stopRestClient() throws IOException { + 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; } } - @Test - public void solrCloudTest() throws IOException, SolrServerException { - try (SolrContainer container = new SolrContainer(SOLR_IMAGE)) { + @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(); @@ -36,9 +39,10 @@ public void solrCloudTest() throws IOException, SolrServerException { } } - @Test - public void solrStandaloneTest() throws IOException, SolrServerException { - try (SolrContainer container = new SolrContainer(SOLR_IMAGE).withZookeeper(false)) { + @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(); @@ -46,11 +50,12 @@ public void solrStandaloneTest() throws IOException, SolrServerException { } } - @Test - public void solrCloudPingTest() throws IOException, SolrServerException { + @ParameterizedTest + @MethodSource("getVersionsToTest") + void solrCloudPingTest(String solrImage) throws IOException, SolrServerException { // solrContainerUsage { // Create the solr container. - SolrContainer container = new SolrContainer(SOLR_IMAGE); + SolrContainer container = new SolrContainer(solrImage); // Start the container. This step might take some time... container.start(); diff --git a/modules/spock/build.gradle b/modules/spock/build.gradle index 98ff9b6520b..2ff27ecbace 100644 --- a/modules/spock/build.gradle +++ b/modules/spock/build.gradle @@ -6,21 +6,28 @@ description = "Testcontainers :: Spock-Extension" dependencies { api project(':testcontainers') - api 'org.spockframework:spock-core:2.3-groovy-4.0' + implementation 'org.spockframework:spock-core:2.3-groovy-4.0' - testImplementation project(':selenium') - testImplementation project(':mysql') - testImplementation project(':postgresql') + testImplementation project(':testcontainers-selenium') + testImplementation project(':testcontainers-mysql') + testImplementation project(':testcontainers-postgresql') - testImplementation 'com.zaxxer:HikariCP:4.0.3' + testImplementation 'com.zaxxer:HikariCP:7.1.0' testImplementation 'org.apache.httpcomponents:httpclient:4.5.14' - testRuntimeOnly 'org.postgresql:postgresql:42.7.1' - testRuntimeOnly 'mysql:mysql-connector-java:8.0.33' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.1' - testRuntimeOnly 'org.junit.platform:junit-platform-testkit:1.10.1' + testRuntimeOnly 'org.postgresql:postgresql:42.7.12' + testRuntimeOnly 'com.mysql:mysql-connector-j:9.6.0' + testRuntimeOnly platform('org.junit:junit-bom:5.14.3') + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testRuntimeOnly 'org.junit.platform:junit-platform-testkit' - testCompileOnly 'org.jetbrains:annotations:24.1.0' + testCompileOnly 'org.jetbrains:annotations:26.1.0' +} + +tasks.withType(GroovyCompile) { + sourceCompatibility = '1.8' + targetCompatibility = '1.8' + options.encoding = 'UTF-8' } sourceJar { @@ -34,10 +41,3 @@ javadocJar { archiveClassifier = 'javadoc' from groovydoc.destinationDir } - -test { - useJUnitPlatform() - testLogging { - events "passed", "skipped", "failed" - } -} diff --git a/modules/spock/src/main/groovy/org/testcontainers/spock/DockerAvailableDetector.groovy b/modules/spock/src/main/groovy/org/testcontainers/spock/DockerAvailableDetector.groovy new file mode 100644 index 00000000000..b64299ffa87 --- /dev/null +++ b/modules/spock/src/main/groovy/org/testcontainers/spock/DockerAvailableDetector.groovy @@ -0,0 +1,15 @@ +package org.testcontainers.spock + +import org.testcontainers.DockerClientFactory + +class DockerAvailableDetector { + + boolean isDockerAvailable() { + try { + DockerClientFactory.instance().client(); + return true; + } catch (Throwable ex) { + return false; + } + } +} diff --git a/modules/spock/src/main/groovy/org/testcontainers/spock/Testcontainers.groovy b/modules/spock/src/main/groovy/org/testcontainers/spock/Testcontainers.groovy index 632b129ec7a..98c2223904b 100644 --- a/modules/spock/src/main/groovy/org/testcontainers/spock/Testcontainers.groovy +++ b/modules/spock/src/main/groovy/org/testcontainers/spock/Testcontainers.groovy @@ -54,4 +54,11 @@ import java.lang.annotation.Target @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; } diff --git a/modules/spock/src/main/groovy/org/testcontainers/spock/TestcontainersExtension.groovy b/modules/spock/src/main/groovy/org/testcontainers/spock/TestcontainersExtension.groovy index 40392210f3a..2654408e901 100644 --- a/modules/spock/src/main/groovy/org/testcontainers/spock/TestcontainersExtension.groovy +++ b/modules/spock/src/main/groovy/org/testcontainers/spock/TestcontainersExtension.groovy @@ -7,8 +7,23 @@ 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) diff --git a/modules/spock/src/main/groovy/org/testcontainers/spock/TestcontainersMethodInterceptor.groovy b/modules/spock/src/main/groovy/org/testcontainers/spock/TestcontainersMethodInterceptor.groovy index 2399a0a6e32..cbef0a87bb2 100644 --- a/modules/spock/src/main/groovy/org/testcontainers/spock/TestcontainersMethodInterceptor.groovy +++ b/modules/spock/src/main/groovy/org/testcontainers/spock/TestcontainersMethodInterceptor.groovy @@ -4,6 +4,7 @@ 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 @@ -24,6 +25,9 @@ class TestcontainersMethodInterceptor extends AbstractMethodInterceptor { def containers = findAllContainers(true) startContainers(containers, invocation) + def dockerCompose = findAllDockerComposeContainers(true) + startDockerComposeContainers(dockerCompose, invocation) + def compose = findAllComposeContainers(true) startComposeContainers(compose, invocation) @@ -35,6 +39,9 @@ class TestcontainersMethodInterceptor extends AbstractMethodInterceptor { def containers = findAllContainers(true) stopContainers(containers, invocation) + def dockerCompose = findAllDockerComposeContainers(true) + stopDockerComposeContainers(dockerCompose, invocation) + def compose = findAllComposeContainers(true) stopComposeContainers(compose, invocation) @@ -46,6 +53,9 @@ class TestcontainersMethodInterceptor extends AbstractMethodInterceptor { def containers = findAllContainers(false) startContainers(containers, invocation) + def dockerCompose = findAllDockerComposeContainers(false) + startDockerComposeContainers(dockerCompose, invocation) + def compose = findAllComposeContainers(false) startComposeContainers(compose, invocation) @@ -58,6 +68,9 @@ class TestcontainersMethodInterceptor extends AbstractMethodInterceptor { def containers = findAllContainers(false) stopContainers(containers, invocation) + def dockerCompose = findAllDockerComposeContainers(false) + stopDockerComposeContainers(dockerCompose, invocation) + def compose = findAllComposeContainers(false) stopComposeContainers(compose, invocation) @@ -70,12 +83,18 @@ class TestcontainersMethodInterceptor extends AbstractMethodInterceptor { } } - private List findAllComposeContainers(boolean 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) @@ -105,20 +124,34 @@ class TestcontainersMethodInterceptor extends AbstractMethodInterceptor { } } - private static void startComposeContainers(List compose, IMethodInvocation invocation) { + 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 stopComposeContainers(List compose, IMethodInvocation invocation) { + 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 diff --git a/modules/spock/src/test/groovy/org/testcontainers/spock/ComposeContainerIT.groovy b/modules/spock/src/test/groovy/org/testcontainers/spock/ComposeContainerIT.groovy index 7294d534275..89de5aed88d 100644 --- a/modules/spock/src/test/groovy/org/testcontainers/spock/ComposeContainerIT.groovy +++ b/modules/spock/src/test/groovy/org/testcontainers/spock/ComposeContainerIT.groovy @@ -2,24 +2,26 @@ 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.ComposeContainer import org.testcontainers.containers.wait.strategy.Wait +import org.testcontainers.utility.DockerImageName import spock.lang.Specification @Testcontainers class ComposeContainerIT extends Specification { - DockerComposeContainer composeContainer = new DockerComposeContainer( + ComposeContainer composeContainer = new ComposeContainer( + DockerImageName.parse("docker:25.0.5"), new File("src/test/resources/docker-compose.yml")) - .withExposedService("whoami_1", 80, Wait.forHttp("/")) + .withExposedService("whoami-1", 80, Wait.forHttp("/")) String host int port def setup() { - host = composeContainer.getServiceHost("whoami_1", 80) - port = composeContainer.getServicePort("whoami_1", 80) + host = composeContainer.getServiceHost("whoami-1", 80) + port = composeContainer.getServicePort("whoami-1", 80) } def "running compose defined container is accessible on configured port"() { diff --git a/modules/spock/src/test/groovy/org/testcontainers/spock/DockerComposeContainerIT.groovy b/modules/spock/src/test/groovy/org/testcontainers/spock/DockerComposeContainerIT.groovy new file mode 100644 index 00000000000..bd27c18f7ab --- /dev/null +++ b/modules/spock/src/test/groovy/org/testcontainers/spock/DockerComposeContainerIT.groovy @@ -0,0 +1,37 @@ +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 + } +} diff --git a/modules/spock/src/test/groovy/org/testcontainers/spock/PostgresContainerIT.groovy b/modules/spock/src/test/groovy/org/testcontainers/spock/PostgresContainerIT.groovy index 19edca4b459..4b28dbef5c2 100644 --- a/modules/spock/src/test/groovy/org/testcontainers/spock/PostgresContainerIT.groovy +++ b/modules/spock/src/test/groovy/org/testcontainers/spock/PostgresContainerIT.groovy @@ -41,6 +41,5 @@ class PostgresContainerIT extends Specification { cleanup: ds.close() } - } // } diff --git a/modules/spock/src/test/groovy/org/testcontainers/spock/SharedComposeContainerIT.groovy b/modules/spock/src/test/groovy/org/testcontainers/spock/SharedComposeContainerIT.groovy index f728212278f..dc71687011f 100644 --- a/modules/spock/src/test/groovy/org/testcontainers/spock/SharedComposeContainerIT.groovy +++ b/modules/spock/src/test/groovy/org/testcontainers/spock/SharedComposeContainerIT.groovy @@ -2,8 +2,9 @@ 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.ComposeContainer import org.testcontainers.containers.wait.strategy.Wait +import org.testcontainers.utility.DockerImageName import spock.lang.Shared import spock.lang.Specification @@ -11,17 +12,18 @@ import spock.lang.Specification class SharedComposeContainerIT extends Specification { @Shared - DockerComposeContainer composeContainer = new DockerComposeContainer( + ComposeContainer composeContainer = new ComposeContainer( + DockerImageName.parse("docker:25.0.5"), new File("src/test/resources/docker-compose.yml")) - .withExposedService("whoami_1", 80, Wait.forHttp("/")) + .withExposedService("whoami-1", 80, Wait.forHttp("/")) String host int port def setup() { - host = composeContainer.getServiceHost("whoami_1", 80) - port = composeContainer.getServicePort("whoami_1", 80) + host = composeContainer.getServiceHost("whoami-1", 80) + port = composeContainer.getServicePort("whoami-1", 80) } def "running compose defined container is accessible on configured port"() { diff --git a/modules/spock/src/test/groovy/org/testcontainers/spock/SharedDockerComposeContainerIT.groovy b/modules/spock/src/test/groovy/org/testcontainers/spock/SharedDockerComposeContainerIT.groovy new file mode 100644 index 00000000000..600be07f9f1 --- /dev/null +++ b/modules/spock/src/test/groovy/org/testcontainers/spock/SharedDockerComposeContainerIT.groovy @@ -0,0 +1,39 @@ +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 + } +} diff --git a/modules/spock/src/test/groovy/org/testcontainers/spock/SpockTestImages.groovy b/modules/spock/src/test/groovy/org/testcontainers/spock/SpockTestImages.groovy index 43cda0b3313..ce16e963548 100644 --- a/modules/spock/src/test/groovy/org/testcontainers/spock/SpockTestImages.groovy +++ b/modules/spock/src/test/groovy/org/testcontainers/spock/SpockTestImages.groovy @@ -6,5 +6,5 @@ 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.16") + DockerImageName TINY_IMAGE = DockerImageName.parse("alpine:3.17") } diff --git a/modules/spock/src/test/groovy/org/testcontainers/spock/TestcontainersExtensionTest.groovy b/modules/spock/src/test/groovy/org/testcontainers/spock/TestcontainersExtensionTest.groovy new file mode 100644 index 00000000000..d8cbdf2e497 --- /dev/null +++ b/modules/spock/src/test/groovy/org/testcontainers/spock/TestcontainersExtensionTest.groovy @@ -0,0 +1,39 @@ +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 {} +} diff --git a/modules/tidb/build.gradle b/modules/tidb/build.gradle index 43ef0603e3b..a59b1f6a84e 100644 --- a/modules/tidb/build.gradle +++ b/modules/tidb/build.gradle @@ -1,10 +1,9 @@ description = "Testcontainers :: JDBC :: TiDB" dependencies { - api project(':jdbc') + api project(':testcontainers-jdbc') - testImplementation project(':jdbc-test') - testRuntimeOnly 'mysql:mysql-connector-java:8.0.33' - - compileOnly 'org.jetbrains:annotations:24.1.0' + testImplementation project(':testcontainers-jdbc-test') + testRuntimeOnly 'com.mysql:mysql-connector-j:9.6.0' + compileOnly 'org.jetbrains:annotations:26.1.0' } diff --git a/modules/tidb/src/test/java/org/testcontainers/jdbc/tidb/TiDBJDBCDriverTest.java b/modules/tidb/src/test/java/org/testcontainers/jdbc/tidb/TiDBJDBCDriverTest.java index 566850fa629..4b3d60a013c 100644 --- a/modules/tidb/src/test/java/org/testcontainers/jdbc/tidb/TiDBJDBCDriverTest.java +++ b/modules/tidb/src/test/java/org/testcontainers/jdbc/tidb/TiDBJDBCDriverTest.java @@ -1,16 +1,12 @@ package org.testcontainers.jdbc.tidb; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; -@RunWith(Parameterized.class) public class TiDBJDBCDriverTest extends AbstractJDBCDriverTest { - @Parameterized.Parameters(name = "{index} - {0}") public static Iterable data() { return Arrays.asList( new Object[][] { { "jdbc:tc:tidb://hostname/databasename", EnumSet.noneOf(Options.class) } } diff --git a/modules/tidb/src/test/java/org/testcontainers/junit/tidb/SimpleTiDBTest.java b/modules/tidb/src/test/java/org/testcontainers/tidb/TiDBContainerTest.java similarity index 80% rename from modules/tidb/src/test/java/org/testcontainers/junit/tidb/SimpleTiDBTest.java rename to modules/tidb/src/test/java/org/testcontainers/tidb/TiDBContainerTest.java index 6b4429afe75..54ded93e69e 100644 --- a/modules/tidb/src/test/java/org/testcontainers/junit/tidb/SimpleTiDBTest.java +++ b/modules/tidb/src/test/java/org/testcontainers/tidb/TiDBContainerTest.java @@ -1,20 +1,22 @@ -package org.testcontainers.junit.tidb; +package org.testcontainers.tidb; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.TiDBTestImages; import org.testcontainers.db.AbstractContainerDatabaseTest; -import org.testcontainers.tidb.TiDBContainer; import java.sql.ResultSet; import java.sql.SQLException; import static org.assertj.core.api.Assertions.assertThat; -public class SimpleTiDBTest extends AbstractContainerDatabaseTest { +class TiDBContainerTest extends AbstractContainerDatabaseTest { @Test - public void testSimple() throws SQLException { - try (TiDBContainer tidb = new TiDBContainer(TiDBTestImages.TIDB_IMAGE)) { + void testSimple() throws SQLException { + try ( // container { + TiDBContainer tidb = new TiDBContainer("pingcap/tidb:v6.1.0") + // } + ) { tidb.start(); ResultSet resultSet = performQuery(tidb, "SELECT 1"); @@ -26,7 +28,7 @@ public void testSimple() throws SQLException { } @Test - public void testExplicitInitScript() throws SQLException { + 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 @@ -40,7 +42,7 @@ public void testExplicitInitScript() throws SQLException { } @Test - public void testWithAdditionalUrlParamInJdbcUrl() { + void testWithAdditionalUrlParamInJdbcUrl() { TiDBContainer tidb = new TiDBContainer(TiDBTestImages.TIDB_IMAGE).withUrlParam("sslmode", "disable"); try { diff --git a/modules/timeplus/build.gradle b/modules/timeplus/build.gradle new file mode 100644 index 00000000000..8acf35ef232 --- /dev/null +++ b/modules/timeplus/build.gradle @@ -0,0 +1,9 @@ +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' +} diff --git a/modules/timeplus/src/main/java/org/testcontainers/timeplus/TimeplusContainer.java b/modules/timeplus/src/main/java/org/testcontainers/timeplus/TimeplusContainer.java new file mode 100644 index 00000000000..0ba28c8f10a --- /dev/null +++ b/modules/timeplus/src/main/java/org/testcontainers/timeplus/TimeplusContainer.java @@ -0,0 +1,125 @@ +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; + } +} diff --git a/modules/timeplus/src/main/java/org/testcontainers/timeplus/TimeplusContainerProvider.java b/modules/timeplus/src/main/java/org/testcontainers/timeplus/TimeplusContainerProvider.java new file mode 100644 index 00000000000..1d71e654a45 --- /dev/null +++ b/modules/timeplus/src/main/java/org/testcontainers/timeplus/TimeplusContainerProvider.java @@ -0,0 +1,32 @@ +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(); + } + } +} diff --git a/modules/timeplus/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider b/modules/timeplus/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider new file mode 100644 index 00000000000..f3122d3d73b --- /dev/null +++ b/modules/timeplus/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider @@ -0,0 +1 @@ +org.testcontainers.timeplus.TimeplusContainerProvider diff --git a/modules/timeplus/src/test/java/org/testcontainers/TimeplusImages.java b/modules/timeplus/src/test/java/org/testcontainers/TimeplusImages.java new file mode 100644 index 00000000000..4079e0b4524 --- /dev/null +++ b/modules/timeplus/src/test/java/org/testcontainers/TimeplusImages.java @@ -0,0 +1,7 @@ +package org.testcontainers; + +import org.testcontainers.utility.DockerImageName; + +public interface TimeplusImages { + DockerImageName TIMEPLUS_IMAGE = DockerImageName.parse("timeplus/timeplusd:2.3.21"); +} diff --git a/modules/timeplus/src/test/java/org/testcontainers/junit/timeplus/TimeplusJDBCDriverTest.java b/modules/timeplus/src/test/java/org/testcontainers/junit/timeplus/TimeplusJDBCDriverTest.java new file mode 100644 index 00000000000..1427f33e5a4 --- /dev/null +++ b/modules/timeplus/src/test/java/org/testcontainers/junit/timeplus/TimeplusJDBCDriverTest.java @@ -0,0 +1,15 @@ +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) } } + ); + } +} diff --git a/modules/timeplus/src/test/java/org/testcontainers/timeplus/TimeplusContainerTest.java b/modules/timeplus/src/test/java/org/testcontainers/timeplus/TimeplusContainerTest.java new file mode 100644 index 00000000000..7ab38c03f87 --- /dev/null +++ b/modules/timeplus/src/test/java/org/testcontainers/timeplus/TimeplusContainerTest.java @@ -0,0 +1,49 @@ +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); + } + } +} diff --git a/modules/timeplus/src/test/resources/logback-test.xml b/modules/timeplus/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..83ef7a1a3ef --- /dev/null +++ b/modules/timeplus/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + diff --git a/modules/toxiproxy/build.gradle b/modules/toxiproxy/build.gradle index 36290b875a6..3e3a63a1fca 100644 --- a/modules/toxiproxy/build.gradle +++ b/modules/toxiproxy/build.gradle @@ -2,8 +2,7 @@ description = "Testcontainers :: Toxiproxy" dependencies { api project(':testcontainers') - api 'eu.rekawek.toxiproxy:toxiproxy-java:2.1.7' + api 'eu.rekawek.toxiproxy:toxiproxy-java:2.1.11' - testImplementation 'redis.clients:jedis:3.0.1' - testImplementation 'org.assertj:assertj-core:3.25.1' + testImplementation 'redis.clients:jedis:7.5.3' } diff --git a/modules/toxiproxy/src/main/java/org/testcontainers/containers/ToxiproxyContainer.java b/modules/toxiproxy/src/main/java/org/testcontainers/containers/ToxiproxyContainer.java index a2a85a95d88..a3f12516f8d 100644 --- a/modules/toxiproxy/src/main/java/org/testcontainers/containers/ToxiproxyContainer.java +++ b/modules/toxiproxy/src/main/java/org/testcontainers/containers/ToxiproxyContainer.java @@ -26,7 +26,10 @@ *
  • 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"); diff --git a/modules/toxiproxy/src/main/java/org/testcontainers/toxiproxy/ToxiproxyContainer.java b/modules/toxiproxy/src/main/java/org/testcontainers/toxiproxy/ToxiproxyContainer.java new file mode 100644 index 00000000000..b4ab7e29c96 --- /dev/null +++ b/modules/toxiproxy/src/main/java/org/testcontainers/toxiproxy/ToxiproxyContainer.java @@ -0,0 +1,54 @@ +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); + } +} diff --git a/modules/toxiproxy/src/test/java/org/testcontainers/containers/ToxiproxyTest.java b/modules/toxiproxy/src/test/java/org/testcontainers/toxiproxy/ToxiproxyContainerTest.java similarity index 80% rename from modules/toxiproxy/src/test/java/org/testcontainers/containers/ToxiproxyTest.java rename to modules/toxiproxy/src/test/java/org/testcontainers/toxiproxy/ToxiproxyContainerTest.java index f1010758fba..2247f135f7b 100644 --- a/modules/toxiproxy/src/test/java/org/testcontainers/containers/ToxiproxyTest.java +++ b/modules/toxiproxy/src/test/java/org/testcontainers/toxiproxy/ToxiproxyContainerTest.java @@ -1,43 +1,53 @@ -package org.testcontainers.containers; +package org.testcontainers.toxiproxy; import eu.rekawek.toxiproxy.Proxy; import eu.rekawek.toxiproxy.ToxiproxyClient; import eu.rekawek.toxiproxy.model.ToxicDirection; -import org.junit.Rule; -import org.junit.Test; +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 ToxiproxyTest { +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 - @Rule + @AutoClose public Network network = Network.newNetwork(); // The target container - this could be anything - @Rule - public GenericContainer redis = new GenericContainer<>("redis:5.0.4") + @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 - @Rule + @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()); @@ -103,6 +113,7 @@ public void testConnectionCut() throws IOException { 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") @@ -113,7 +124,7 @@ public void testConnectionCut() throws IOException { @Test public void testMultipleProxiesCanBeCreated() throws IOException { try ( - GenericContainer secondRedis = new GenericContainer<>("redis:5.0.4") + GenericContainer secondRedis = new GenericContainer<>("redis:6-alpine") .withExposedPorts(6379) .withNetwork(network) .withNetworkAliases("redis2") @@ -148,43 +159,16 @@ public void testMultipleProxiesCanBeCreated() throws IOException { } } - @Test - public void testOriginalAndMappedPorts() { - final ToxiproxyContainer.ContainerProxy proxy = toxiproxy.getProxy("hostname", 7070); - - final int portViaToxiproxy = proxy.getOriginalProxyPort(); - assertThat(portViaToxiproxy).as("original port is correct").isEqualTo(8666); - - final ToxiproxyContainer.ContainerProxy proxy1 = toxiproxy.getProxy("hostname1", 8080); - assertThat(proxy1.getOriginalProxyPort()).as("original port is correct").isEqualTo(8667); - assertThat(proxy1.getProxyPort()) - .as("mapped port is correct") - .isEqualTo(toxiproxy.getMappedPort(proxy1.getOriginalProxyPort())); - - final ToxiproxyContainer.ContainerProxy proxy2 = toxiproxy.getProxy("hostname2", 9090); - assertThat(proxy2.getOriginalProxyPort()).as("original port is correct").isEqualTo(8668); - assertThat(proxy2.getProxyPort()) - .as("mapped port is correct") - .isEqualTo(toxiproxy.getMappedPort(proxy2.getOriginalProxyPort())); - } - - @Test - public void testProxyName() { - final ToxiproxyContainer.ContainerProxy proxy = toxiproxy.getProxy("hostname", 7070); - - assertThat(proxy.getName()).as("proxy name is hostname and port").isEqualTo("hostname:7070"); - } - private void checkCallWithLatency( Jedis jedis, final String description, int expectedMinLatency, long expectedMaxLatency ) { - final long start = System.currentTimeMillis(); + final long start = System.nanoTime(); String s = jedis.get("somekey"); - final long end = System.currentTimeMillis(); - final long duration = end - start; + 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) diff --git a/modules/trino/build.gradle b/modules/trino/build.gradle index afa4e079663..12d3d05f56b 100644 --- a/modules/trino/build.gradle +++ b/modules/trino/build.gradle @@ -1,9 +1,9 @@ description = "Testcontainers :: JDBC :: Trino" dependencies { - api project(':jdbc') + api project(':testcontainers-jdbc') - testImplementation project(':jdbc-test') - testRuntimeOnly 'io.trino:trino-jdbc:436' - compileOnly 'org.jetbrains:annotations:24.1.0' + testImplementation project(':testcontainers-jdbc-test') + testRuntimeOnly 'io.trino:trino-jdbc:483' + compileOnly 'org.jetbrains:annotations:26.1.0' } diff --git a/modules/trino/src/main/java/org/testcontainers/containers/TrinoContainer.java b/modules/trino/src/main/java/org/testcontainers/containers/TrinoContainer.java index 19da8d9252a..4f61b6e2352 100644 --- a/modules/trino/src/main/java/org/testcontainers/containers/TrinoContainer.java +++ b/modules/trino/src/main/java/org/testcontainers/containers/TrinoContainer.java @@ -15,7 +15,10 @@ * 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"; diff --git a/modules/trino/src/main/java/org/testcontainers/trino/TrinoContainer.java b/modules/trino/src/main/java/org/testcontainers/trino/TrinoContainer.java new file mode 100644 index 00000000000..145ae4b454c --- /dev/null +++ b/modules/trino/src/main/java/org/testcontainers/trino/TrinoContainer.java @@ -0,0 +1,108 @@ +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(""); + } +} diff --git a/modules/trino/src/test/java/org/testcontainers/TrinoTestImages.java b/modules/trino/src/test/java/org/testcontainers/TrinoTestImages.java index 0ac56e54129..e6770bd8993 100644 --- a/modules/trino/src/test/java/org/testcontainers/TrinoTestImages.java +++ b/modules/trino/src/test/java/org/testcontainers/TrinoTestImages.java @@ -4,5 +4,6 @@ public interface TrinoTestImages { DockerImageName TRINO_TEST_IMAGE = DockerImageName.parse("trinodb/trino:352"); + DockerImageName TRINO_PREVIOUS_VERSION_TEST_IMAGE = DockerImageName.parse("trinodb/trino:351"); } diff --git a/modules/trino/src/test/java/org/testcontainers/jdbc/trino/TrinoJDBCDriverTest.java b/modules/trino/src/test/java/org/testcontainers/jdbc/trino/TrinoJDBCDriverTest.java index 9ec98c2e6d7..7674d1bc459 100644 --- a/modules/trino/src/test/java/org/testcontainers/jdbc/trino/TrinoJDBCDriverTest.java +++ b/modules/trino/src/test/java/org/testcontainers/jdbc/trino/TrinoJDBCDriverTest.java @@ -1,16 +1,12 @@ package org.testcontainers.jdbc.trino; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; import java.util.EnumSet; -@RunWith(Parameterized.class) -public class TrinoJDBCDriverTest extends AbstractJDBCDriverTest { +class TrinoJDBCDriverTest extends AbstractJDBCDriverTest { - @Parameterized.Parameters(name = "{index} - {0}") public static Iterable data() { return Arrays.asList( new Object[][] { // diff --git a/modules/trino/src/test/java/org/testcontainers/containers/TrinoContainerTest.java b/modules/trino/src/test/java/org/testcontainers/trino/TrinoContainerTest.java similarity index 82% rename from modules/trino/src/test/java/org/testcontainers/containers/TrinoContainerTest.java rename to modules/trino/src/test/java/org/testcontainers/trino/TrinoContainerTest.java index 6ef733ca972..c76c1bbb322 100644 --- a/modules/trino/src/test/java/org/testcontainers/containers/TrinoContainerTest.java +++ b/modules/trino/src/test/java/org/testcontainers/trino/TrinoContainerTest.java @@ -1,6 +1,6 @@ -package org.testcontainers.containers; +package org.testcontainers.trino; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.TrinoTestImages; import java.sql.Connection; @@ -9,11 +9,14 @@ import static org.assertj.core.api.Assertions.assertThat; -public class TrinoContainerTest { +class TrinoContainerTest { @Test - public void testSimple() throws Exception { - try (TrinoContainer trino = new TrinoContainer(TrinoTestImages.TRINO_TEST_IMAGE)) { + void testSimple() throws Exception { + try ( // container { + TrinoContainer trino = new TrinoContainer("trinodb/trino:352") + // } + ) { trino.start(); try ( Connection connection = trino.createConnection(); @@ -21,16 +24,14 @@ public void testSimple() throws Exception { 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(TrinoContainer.DEFAULT_TAG); + assertThat(resultSet.getString("node_version")).as("Trino version").isEqualTo("352"); assertContainerHasCorrectExposedAndLivenessCheckPorts(trino); } } } @Test - public void testSpecificVersion() throws Exception { + void testSpecificVersion() throws Exception { try (TrinoContainer trino = new TrinoContainer(TrinoTestImages.TRINO_PREVIOUS_VERSION_TEST_IMAGE)) { trino.start(); try ( @@ -47,7 +48,7 @@ public void testSpecificVersion() throws Exception { } @Test - public void testInitScript() throws Exception { + void testInitScript() throws Exception { try (TrinoContainer trino = new TrinoContainer(TrinoTestImages.TRINO_TEST_IMAGE)) { trino.withInitScript("initial.sql"); trino.start(); diff --git a/modules/typesense/build.gradle b/modules/typesense/build.gradle new file mode 100644 index 00000000000..87aef78cad1 --- /dev/null +++ b/modules/typesense/build.gradle @@ -0,0 +1,7 @@ +description = "Testcontainers :: Typesense" + +dependencies { + api project(':testcontainers') + + testImplementation 'org.typesense:typesense-java:2.1.0' +} diff --git a/modules/typesense/src/main/java/org/testcontainers/typesense/TypesenseContainer.java b/modules/typesense/src/main/java/org/testcontainers/typesense/TypesenseContainer.java new file mode 100644 index 00000000000..f5ba92aee02 --- /dev/null +++ b/modules/typesense/src/main/java/org/testcontainers/typesense/TypesenseContainer.java @@ -0,0 +1,58 @@ +package org.testcontainers.typesense; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +/** + * Testcontainers implementation for Typesense. + *

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

    + * Exposed ports: 8108 + */ +public class TypesenseContainer extends GenericContainer { + + private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("typesense/typesense"); + + private static final int PORT = 8108; + + private static final String DEFAULT_API_KEY = "testcontainers"; + + private String apiKey = DEFAULT_API_KEY; + + public TypesenseContainer(String dockerImageName) { + this(DockerImageName.parse(dockerImageName)); + } + + public TypesenseContainer(DockerImageName dockerImageName) { + super(dockerImageName); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + withExposedPorts(PORT); + withEnv("TYPESENSE_DATA_DIR", "/tmp"); + waitingFor( + Wait + .forHttp("/health") + .forStatusCode(200) + .forResponsePredicate(response -> response.contains("\"ok\":true")) + ); + } + + @Override + protected void configure() { + withEnv("TYPESENSE_API_KEY", this.apiKey); + } + + public TypesenseContainer withApiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } + + public String getHttpPort() { + return String.valueOf(getMappedPort(PORT)); + } + + public String getApiKey() { + return this.apiKey; + } +} diff --git a/modules/typesense/src/test/java/org/testcontainers/typesense/TypesenseContainerTest.java b/modules/typesense/src/test/java/org/testcontainers/typesense/TypesenseContainerTest.java new file mode 100644 index 00000000000..f833814a077 --- /dev/null +++ b/modules/typesense/src/test/java/org/testcontainers/typesense/TypesenseContainerTest.java @@ -0,0 +1,49 @@ +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); + } + } +} diff --git a/modules/typesense/src/test/resources/logback-test.xml b/modules/typesense/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..83ef7a1a3ef --- /dev/null +++ b/modules/typesense/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + diff --git a/modules/vault/build.gradle b/modules/vault/build.gradle index 5f8d85af7ec..bbf7929c4e7 100644 --- a/modules/vault/build.gradle +++ b/modules/vault/build.gradle @@ -4,7 +4,5 @@ dependencies { api project(':testcontainers') testImplementation 'com.bettercloud:vault-java-driver:5.1.0' - testImplementation 'io.rest-assured:rest-assured:5.4.0' - testImplementation 'org.assertj:assertj-core:3.25.1' - + testImplementation 'io.rest-assured:rest-assured:5.5.7' } diff --git a/modules/vault/src/main/java/org/testcontainers/vault/VaultContainer.java b/modules/vault/src/main/java/org/testcontainers/vault/VaultContainer.java index 2e6396f7b93..f29595b7ebb 100644 --- a/modules/vault/src/main/java/org/testcontainers/vault/VaultContainer.java +++ b/modules/vault/src/main/java/org/testcontainers/vault/VaultContainer.java @@ -171,7 +171,7 @@ public SELF withLogLevel(VaultLogLevel level) { * {@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 specifed path + * @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 diff --git a/modules/vault/src/test/java/org/testcontainers/vault/VaultClientTest.java b/modules/vault/src/test/java/org/testcontainers/vault/VaultClientTest.java index eebf73a5e62..92f4a6632b9 100644 --- a/modules/vault/src/test/java/org/testcontainers/vault/VaultClientTest.java +++ b/modules/vault/src/test/java/org/testcontainers/vault/VaultClientTest.java @@ -4,19 +4,19 @@ import com.bettercloud.vault.VaultConfig; import com.bettercloud.vault.VaultException; import com.bettercloud.vault.response.LogicalResponse; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.HashMap; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; -public class VaultClientTest { +class VaultClientTest { private static final String VAULT_TOKEN = "my-root-token"; @Test - public void writeAndReadMultipleValues() throws VaultException { + void writeAndReadMultipleValues() throws VaultException { try (VaultContainer vaultContainer = new VaultContainer<>("vault:1.1.3").withVaultToken(VAULT_TOKEN)) { vaultContainer.start(); diff --git a/modules/vault/src/test/java/org/testcontainers/vault/VaultContainerTest.java b/modules/vault/src/test/java/org/testcontainers/vault/VaultContainerTest.java index fe578bfe83b..cc2d8b37d73 100644 --- a/modules/vault/src/test/java/org/testcontainers/vault/VaultContainerTest.java +++ b/modules/vault/src/test/java/org/testcontainers/vault/VaultContainerTest.java @@ -4,8 +4,9 @@ import com.bettercloud.vault.VaultConfig; import com.bettercloud.vault.response.LogicalResponse; import io.restassured.response.Response; -import org.junit.ClassRule; -import org.junit.Test; +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; @@ -18,11 +19,10 @@ * 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. */ -public class VaultContainerTest { +class VaultContainerTest { private static final String VAULT_TOKEN = "my-root-token"; - @ClassRule // vaultContainer { public static VaultContainer vaultContainer = new VaultContainer<>("hashicorp/vault:1.13") .withVaultToken(VAULT_TOKEN) @@ -35,8 +35,18 @@ public class VaultContainerTest { // } + @BeforeAll + static void setUp() { + vaultContainer.start(); + } + + @AfterAll + static void tearDown() { + vaultContainer.stop(); + } + @Test - public void readFirstSecretPathWithCli() throws Exception { + void readFirstSecretPathWithCli() throws Exception { GenericContainer.ExecResult result = vaultContainer.execInContainer( "vault", "kv", @@ -48,7 +58,7 @@ public void readFirstSecretPathWithCli() throws Exception { } @Test - public void readSecondSecretPathWithCli() throws Exception { + void readSecondSecretPathWithCli() throws Exception { GenericContainer.ExecResult result = vaultContainer.execInContainer( "vault", "kv", @@ -66,7 +76,7 @@ public void readSecondSecretPathWithCli() throws Exception { } @Test - public void readFirstSecretPathOverHttpApi() { + void readFirstSecretPathOverHttpApi() { Response response = given() .header("X-Vault-Token", VAULT_TOKEN) .when() @@ -76,7 +86,7 @@ public void readFirstSecretPathOverHttpApi() { } @Test - public void readSecondSecretPathOverHttpApi() throws InterruptedException { + void readSecondSecretPathOverHttpApi() throws InterruptedException { Response response = given() .header("X-Vault-Token", VAULT_TOKEN) .when() @@ -90,7 +100,7 @@ public void readSecondSecretPathOverHttpApi() throws InterruptedException { } @Test - public void readTransitKeyOverHttpApi() throws InterruptedException { + void readTransitKeyOverHttpApi() throws InterruptedException { Response response = given() .header("X-Vault-Token", VAULT_TOKEN) .when() @@ -102,7 +112,7 @@ public void readTransitKeyOverHttpApi() throws InterruptedException { @Test // readWithLibrary { - public void readFirstSecretPathOverClientLibrary() throws Exception { + void readFirstSecretPathOverClientLibrary() throws Exception { final VaultConfig config = new VaultConfig() .address(vaultContainer.getHttpHostAddress()) .token(VAULT_TOKEN) @@ -118,7 +128,7 @@ public void readFirstSecretPathOverClientLibrary() throws Exception { // } @Test - public void readSecondSecretPathOverClientLibrary() throws Exception { + void readSecondSecretPathOverClientLibrary() throws Exception { final VaultConfig config = new VaultConfig() .address(vaultContainer.getHttpHostAddress()) .token(VAULT_TOKEN) @@ -135,7 +145,7 @@ public void readSecondSecretPathOverClientLibrary() throws Exception { } @Test - public void writeSecretOverClientLibrary() throws Exception { + void writeSecretOverClientLibrary() throws Exception { final VaultConfig config = new VaultConfig() .address(vaultContainer.getHttpHostAddress()) .token(VAULT_TOKEN) diff --git a/modules/weaviate/build.gradle b/modules/weaviate/build.gradle new file mode 100644 index 00000000000..404329b161f --- /dev/null +++ b/modules/weaviate/build.gradle @@ -0,0 +1,20 @@ +description = "Testcontainers :: Weaviate" + +dependencies { + api project(':testcontainers') + + testImplementation 'io.weaviate:client6:6.3.0' +} + +test { + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(17) + } +} + +compileTestJava { + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(17) + } + options.release.set(17) +} diff --git a/modules/weaviate/src/main/java/org/testcontainers/weaviate/WeaviateContainer.java b/modules/weaviate/src/main/java/org/testcontainers/weaviate/WeaviateContainer.java new file mode 100644 index 00000000000..dbc18a49a22 --- /dev/null +++ b/modules/weaviate/src/main/java/org/testcontainers/weaviate/WeaviateContainer.java @@ -0,0 +1,58 @@ +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 int HTTP_PORT = 8080; + + private static final int GRPC_PORT = 50051; + + 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(HTTP_PORT, GRPC_PORT); + withEnv("AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED", "true"); + withEnv("PERSISTENCE_DATA_PATH", "/var/lib/weaviate"); + waitingFor(Wait.forHttp("/v1/.well-known/ready").forPort(HTTP_PORT).forStatusCode(200)); + } + + public String getHttpHostAddress() { + return getHost() + ":" + getHttpPort(); + } + + public Integer getHttpPort() { + return getMappedPort(HTTP_PORT); + } + + public String getGrpcHostAddress() { + return getHost() + ":" + getGrpcPort(); + } + + public Integer getGrpcPort() { + return getMappedPort(GRPC_PORT); + } +} diff --git a/modules/weaviate/src/test/java/org/testcontainers/weaviate/WeaviateContainerTest.java b/modules/weaviate/src/test/java/org/testcontainers/weaviate/WeaviateContainerTest.java new file mode 100644 index 00000000000..098a1561913 --- /dev/null +++ b/modules/weaviate/src/test/java/org/testcontainers/weaviate/WeaviateContainerTest.java @@ -0,0 +1,78 @@ +package org.testcontainers.weaviate; + +import io.weaviate.client6.v1.api.InstanceMetadata; +import io.weaviate.client6.v1.api.WeaviateClient; +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() throws Exception { + try ( // container { + WeaviateContainer weaviate = new WeaviateContainer("cr.weaviate.io/semitechnologies/weaviate:1.32.0") + // } + ) { + weaviate.start(); + try ( + WeaviateClient client = WeaviateClient.connectToCustom(conn -> { + return conn + .scheme("http") + .httpHost(weaviate.getHost()) + .httpPort(weaviate.getHttpPort()) + .grpcHost(weaviate.getHost()) + .grpcPort(weaviate.getGrpcPort()); + }) + ) { + InstanceMetadata meta = client.meta(); + assertThat(meta.version()).isEqualTo("1.32.0"); + } + } + } + + @Test + void testWeaviateWithModules() throws Exception { + 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.32.0").withEnv(env)) { + weaviate.start(); + try ( + WeaviateClient client = WeaviateClient.connectToCustom(conn -> { + return conn + .scheme("http") + .httpHost(weaviate.getHost()) + .httpPort(weaviate.getHttpPort()) + .grpcHost(weaviate.getHost()) + .grpcPort(weaviate.getGrpcPort()); + }) + ) { + InstanceMetadata meta = client.meta(); + assertThat(meta.version()).isEqualTo("1.32.0"); + Map modules = meta.modules(); + 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()); + }); + } + } + } +} diff --git a/modules/weaviate/src/test/resources/logback-test.xml b/modules/weaviate/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..83ef7a1a3ef --- /dev/null +++ b/modules/weaviate/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + + diff --git a/modules/yugabytedb/build.gradle b/modules/yugabytedb/build.gradle index 100620ab6f9..4351653b4f3 100644 --- a/modules/yugabytedb/build.gradle +++ b/modules/yugabytedb/build.gradle @@ -1,10 +1,11 @@ description = "Testcontainers :: JDBC :: YugabyteDB" dependencies { - api project(':jdbc') - testImplementation project(':jdbc-test') + api project(':testcontainers-jdbc') + + testImplementation project(':testcontainers-jdbc-test') // YCQL driver - testImplementation 'com.yugabyte:java-driver-core:4.15.0-yb-1' + testImplementation 'com.yugabyte:java-driver-core:4.19.0-yb-1' // YSQL driver - testRuntimeOnly 'com.yugabyte:jdbc-yugabytedb:42.3.5-yb-4' + testRuntimeOnly 'com.yugabyte:jdbc-yugabytedb:42.7.3-yb-4' } diff --git a/modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteDBYCQLContainer.java b/modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteDBYCQLContainer.java index d193d19eef1..d79812f247f 100644 --- a/modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteDBYCQLContainer.java +++ b/modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteDBYCQLContainer.java @@ -74,7 +74,7 @@ public Set getLivenessCheckPortNumbers() { * 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 - * initilaize the database with those custom values + * initialize the database with those custom values */ @Override protected void configure() { @@ -123,7 +123,7 @@ public YugabyteDBYCQLContainer withPassword(final String password) { } /** - * Executes the initilization script + * Executes the initialization script * @param containerInfo containerInfo */ @Override diff --git a/modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteDBYSQLContainer.java b/modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteDBYSQLContainer.java index e3b39e780f0..aae3f67b313 100644 --- a/modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteDBYSQLContainer.java +++ b/modules/yugabytedb/src/main/java/org/testcontainers/containers/YugabyteDBYSQLContainer.java @@ -70,7 +70,7 @@ public Set getLivenessCheckPortNumbers() { * 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 - * initilaize the database with those custom values + * initialize the database with those custom values */ @Override diff --git a/modules/yugabytedb/src/test/java/org/testcontainers/jdbc/yugabytedb/YugabyteDBYSQLJDBCDriverTest.java b/modules/yugabytedb/src/test/java/org/testcontainers/jdbc/yugabytedb/YugabyteDBYSQLJDBCDriverTest.java index a7ecab34aba..37129e8f899 100644 --- a/modules/yugabytedb/src/test/java/org/testcontainers/jdbc/yugabytedb/YugabyteDBYSQLJDBCDriverTest.java +++ b/modules/yugabytedb/src/test/java/org/testcontainers/jdbc/yugabytedb/YugabyteDBYSQLJDBCDriverTest.java @@ -1,7 +1,5 @@ package org.testcontainers.jdbc.yugabytedb; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import org.testcontainers.jdbc.AbstractJDBCDriverTest; import java.util.Arrays; @@ -10,10 +8,8 @@ /** * YugabyteDB YSQL API JDBC connectivity driver test class */ -@RunWith(Parameterized.class) -public class YugabyteDBYSQLJDBCDriverTest extends AbstractJDBCDriverTest { +class YugabyteDBYSQLJDBCDriverTest extends AbstractJDBCDriverTest { - @Parameterized.Parameters(name = "{index} - {0}") public static Iterable data() { return Arrays.asList( new Object[][] { diff --git a/modules/yugabytedb/src/test/java/org/testcontainers/junit/yugabytedb/YugabyteDBYCQLTest.java b/modules/yugabytedb/src/test/java/org/testcontainers/junit/yugabytedb/YugabyteDBYCQLTest.java index fe4723b409f..24e6b2c24dd 100644 --- a/modules/yugabytedb/src/test/java/org/testcontainers/junit/yugabytedb/YugabyteDBYCQLTest.java +++ b/modules/yugabytedb/src/test/java/org/testcontainers/junit/yugabytedb/YugabyteDBYCQLTest.java @@ -2,7 +2,7 @@ import com.datastax.oss.driver.api.core.CqlSession; import com.datastax.oss.driver.api.core.cql.ResultSet; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.YugabyteDBYCQLContainer; import org.testcontainers.utility.DockerImageName; @@ -11,7 +11,7 @@ /** * YugabyteDB YCQL API unit test class */ -public class YugabyteDBYCQLTest { +class YugabyteDBYCQLTest { private static final String IMAGE_NAME = "yugabytedb/yugabyte:2.14.4.0-b26"; @@ -20,7 +20,7 @@ public class YugabyteDBYCQLTest { private static final DockerImageName YBDB_TEST_IMAGE = DockerImageName.parse(IMAGE_NAME); @Test - public void testSmoke() { + void testSmoke() { try ( // creatingYCQLContainer { final YugabyteDBYCQLContainer ycqlContainer = new YugabyteDBYCQLContainer( @@ -30,9 +30,7 @@ public void testSmoke() { .withPassword("cassandra") // } ) { - // startingYCQLContainer { ycqlContainer.start(); - // } assertThat(performQuery(ycqlContainer, "SELECT release_version FROM system.local").wasApplied()) .as("A sample test query succeeds") .isTrue(); @@ -40,7 +38,7 @@ public void testSmoke() { } @Test - public void testCustomKeyspace() { + void testCustomKeyspace() { String key = "random"; try ( final YugabyteDBYCQLContainer ycqlContainer = new YugabyteDBYCQLContainer(YBDB_TEST_IMAGE) @@ -63,7 +61,7 @@ public void testCustomKeyspace() { } @Test - public void testAuthenticationEnabled() { + void testAuthenticationEnabled() { String role = "random"; try ( final YugabyteDBYCQLContainer ycqlContainer = new YugabyteDBYCQLContainer(YBDB_TEST_IMAGE) @@ -82,7 +80,7 @@ public void testAuthenticationEnabled() { } @Test - public void testAuthenticationDisabled() { + void testAuthenticationDisabled() { try ( final YugabyteDBYCQLContainer ycqlContainer = new YugabyteDBYCQLContainer(YBDB_TEST_IMAGE) .withPassword("cassandra") @@ -96,7 +94,7 @@ public void testAuthenticationDisabled() { } @Test - public void testInitScript() { + void testInitScript() { String key = "random"; try ( final YugabyteDBYCQLContainer ycqlContainer = new YugabyteDBYCQLContainer(YBDB_TEST_IMAGE) @@ -113,7 +111,7 @@ public void testInitScript() { } @Test - public void shouldStartWhenContainerIpIsUsedInWaitStrategy() { + void shouldStartWhenContainerIpIsUsedInWaitStrategy() { try ( final YugabyteDBYCQLContainer ycqlContainer = new YugabyteDBYCQLContainer(IMAGE_NAME_2_18) .withUsername("cassandra") diff --git a/modules/yugabytedb/src/test/java/org/testcontainers/junit/yugabytedb/YugabyteDBYSQLTest.java b/modules/yugabytedb/src/test/java/org/testcontainers/junit/yugabytedb/YugabyteDBYSQLTest.java index 94cfaa7f2e4..04d1448cfba 100644 --- a/modules/yugabytedb/src/test/java/org/testcontainers/junit/yugabytedb/YugabyteDBYSQLTest.java +++ b/modules/yugabytedb/src/test/java/org/testcontainers/junit/yugabytedb/YugabyteDBYSQLTest.java @@ -1,6 +1,6 @@ package org.testcontainers.junit.yugabytedb; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.YugabyteDBYSQLContainer; import org.testcontainers.db.AbstractContainerDatabaseTest; import org.testcontainers.utility.DockerImageName; @@ -12,14 +12,14 @@ /** * YugabyteDB YSQL API unit test class */ -public class YugabyteDBYSQLTest extends AbstractContainerDatabaseTest { +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 - public void testSmoke() throws SQLException { + void testSmoke() throws SQLException { try ( // creatingYSQLContainer { final YugabyteDBYSQLContainer ysqlContainer = new YugabyteDBYSQLContainer( @@ -27,9 +27,7 @@ public void testSmoke() throws SQLException { ) // } ) { - // startingYSQLContainer { ysqlContainer.start(); - // } assertThat(performQuery(ysqlContainer, "SELECT 1").getInt(1)) .as("A sample test query succeeds") .isEqualTo(1); @@ -37,7 +35,7 @@ public void testSmoke() throws SQLException { } @Test - public void testCustomDatabase() throws SQLException { + void testCustomDatabase() throws SQLException { String key = "random"; try ( final YugabyteDBYSQLContainer ysqlContainer = new YugabyteDBYSQLContainer(YBDB_TEST_IMAGE) @@ -51,7 +49,7 @@ public void testCustomDatabase() throws SQLException { } @Test - public void testInitScript() throws SQLException { + void testInitScript() throws SQLException { try ( final YugabyteDBYSQLContainer ysqlContainer = new YugabyteDBYSQLContainer(YBDB_TEST_IMAGE) .withInitScript("init/init_yql.sql") @@ -64,7 +62,7 @@ public void testInitScript() throws SQLException { } @Test - public void testWithAdditionalUrlParamInJdbcUrl() { + void testWithAdditionalUrlParamInJdbcUrl() { try ( final YugabyteDBYSQLContainer ysqlContainer = new YugabyteDBYSQLContainer(YBDB_TEST_IMAGE) .withUrlParam("sslmode", "disable") @@ -82,7 +80,7 @@ public void testWithAdditionalUrlParamInJdbcUrl() { } @Test - public void testWithCustomRole() throws SQLException { + void testWithCustomRole() throws SQLException { try ( final YugabyteDBYSQLContainer ysqlContainer = new YugabyteDBYSQLContainer(YBDB_TEST_IMAGE) .withDatabaseName("yugabyte") @@ -97,7 +95,7 @@ public void testWithCustomRole() throws SQLException { } @Test - public void testWaitStrategy() throws SQLException { + void testWaitStrategy() throws SQLException { try (final YugabyteDBYSQLContainer ysqlContainer = new YugabyteDBYSQLContainer(YBDB_TEST_IMAGE)) { ysqlContainer.start(); assertThat(performQuery(ysqlContainer, "SELECT 1").getInt(1)) diff --git a/settings.gradle b/settings.gradle index 9326635359e..374dbbb00dc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,13 +5,13 @@ buildscript { } } dependencies { - classpath "com.gradle.enterprise:com.gradle.enterprise.gradle.plugin:3.16.1" - classpath "com.gradle:common-custom-user-data-gradle-plugin:1.12.1" + classpath "com.gradle:develocity-gradle-plugin:4.4.1" + classpath "com.gradle:common-custom-user-data-gradle-plugin:2.6.0" classpath "org.gradle.toolchains:foojay-resolver:0.8.0" } } -apply plugin: 'com.gradle.enterprise' +apply plugin: 'com.gradle.develocity' apply plugin: "com.gradle.common-custom-user-data-gradle-plugin" apply plugin: "org.gradle.toolchains.foojay-resolver-convention" @@ -23,12 +23,10 @@ include "testcontainers" project(':testcontainers').projectDir = "$rootDir/core" as File file('modules').eachDir { dir -> - include dir.name - project(":${dir.name}").projectDir = dir + include "testcontainers-${dir.name}" + project(":testcontainers-${dir.name}").projectDir = dir } -include ':docs:examples:junit4:generic' -include ':docs:examples:junit4:redis' include ':docs:examples:junit5:redis' include ':docs:examples:spock:redis' @@ -40,24 +38,23 @@ buildCache { local { enabled = !isCI } - remote(HttpBuildCache) { - push = isCI && !System.getenv("READ_ONLY_REMOTE_GRADLE_CACHE") && System.getenv("GRADLE_ENTERPRISE_CACHE_PASSWORD") + remote(develocity.buildCache) { enabled = true - url = 'https://ge.testcontainers.org/cache/' - credentials { - username = 'ci' - password = System.getenv("GRADLE_ENTERPRISE_CACHE_PASSWORD") - } + // Check access key presence to avoid build cache errors on PR builds when access key is not present + push = isCI && !System.getenv("READ_ONLY_REMOTE_GRADLE_CACHE") && System.getenv("DEVELOCITY_ACCESS_KEY") != null } } -gradleEnterprise { +develocity { + server = "https://community.develocity.cloud" + projectId = "testcontainers" buildScan { - server = "https://ge.testcontainers.org/" - publishAlways() - publishIfAuthenticated() + publishing.onlyIf { + it.authenticated + } uploadInBackground = !isCI - captureTaskInputFiles = true + obfuscation { + ipAddresses { it.collect { "0.0.0.0" } } + } } - } diff --git a/smoke-test/build.gradle b/smoke-test/build.gradle index 830cfaf0b66..781d6e87792 100644 --- a/smoke-test/build.gradle +++ b/smoke-test/build.gradle @@ -1,6 +1,6 @@ // empty build.gradle for dependabot plugins { - id 'com.diffplug.spotless' version '6.13.0' apply false + id 'com.diffplug.spotless' version '6.22.0' apply false } apply from: "$rootDir/../gradle/ci-support.gradle" @@ -14,8 +14,18 @@ subprojects { mavenCentral() } + test { + defaultCharacterEncoding = "UTF-8" + testLogging { + displayGranularity 1 + showStackTraces = true + exceptionFormat = 'full' + events "STARTED", "PASSED", "FAILED", "SKIPPED" + } + } + checkstyle { - toolVersion = "9.3" + toolVersion = "10.23.0" configFile = rootProject.file('../config/checkstyle/checkstyle.xml') } } diff --git a/smoke-test/gradle/wrapper/gradle-wrapper.jar b/smoke-test/gradle/wrapper/gradle-wrapper.jar index d64cd491770..1b33c55baab 100644 Binary files a/smoke-test/gradle/wrapper/gradle-wrapper.jar and b/smoke-test/gradle/wrapper/gradle-wrapper.jar differ diff --git a/smoke-test/gradle/wrapper/gradle-wrapper.properties b/smoke-test/gradle/wrapper/gradle-wrapper.properties index db8c3baafe3..78cb6e16a49 100644 --- a/smoke-test/gradle/wrapper/gradle-wrapper.properties +++ b/smoke-test/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=9d926787066a081739e8200858338b4a69e837c3a821a33aca9db09dd4a41026 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/smoke-test/gradlew b/smoke-test/gradlew index 1aa94a42690..23d15a93670 100755 --- a/smoke-test/gradlew +++ b/smoke-test/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -112,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -203,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * 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. @@ -211,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/smoke-test/gradlew.bat b/smoke-test/gradlew.bat index 6689b85beec..5eed7ee8452 100644 --- a/smoke-test/gradlew.bat +++ b/smoke-test/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,22 +59,22 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%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 diff --git a/smoke-test/settings.gradle b/smoke-test/settings.gradle index b82f031e8ca..6334742441a 100644 --- a/smoke-test/settings.gradle +++ b/smoke-test/settings.gradle @@ -5,13 +5,12 @@ buildscript { } } dependencies { - classpath "gradle.plugin.ch.myniva.gradle:s3-build-cache:0.10.0" - classpath "com.gradle.enterprise:com.gradle.enterprise.gradle.plugin:3.14.1" - classpath "com.gradle:common-custom-user-data-gradle-plugin:1.11.1" + 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.enterprise' +apply plugin: 'com.gradle.develocity' apply plugin: "com.gradle.common-custom-user-data-gradle-plugin" rootProject.name = 'testcontainers-smoke-tests' @@ -27,24 +26,20 @@ buildCache { local { enabled = !isCI } - remote(HttpBuildCache) { - push = isCI && !System.getenv("READ_ONLY_REMOTE_GRADLE_CACHE") && System.getenv("GRADLE_ENTERPRISE_CACHE_PASSWORD") + remote(develocity.buildCache) { + push = isCI && !System.getenv("READ_ONLY_REMOTE_GRADLE_CACHE") && System.getenv("DEVELOCITY_ACCESS_KEY") enabled = true - url = 'https://ge.testcontainers.org/cache/' - credentials { - username = 'ci' - password = System.getenv("GRADLE_ENTERPRISE_CACHE_PASSWORD") - } } } -gradleEnterprise { +develocity { buildScan { server = "https://ge.testcontainers.org/" - publishAlways() - publishIfAuthenticated() + publishing.onlyIf { + it.authenticated + } uploadInBackground = !isCI - captureTaskInputFiles = true + capture.fileFingerprints = true } } diff --git a/smoke-test/turbo-mode/build.gradle b/smoke-test/turbo-mode/build.gradle index a84c1d5fb48..fb3f09244fd 100644 --- a/smoke-test/turbo-mode/build.gradle +++ b/smoke-test/turbo-mode/build.gradle @@ -4,9 +4,10 @@ plugins { dependencies { testImplementation 'org.testcontainers:testcontainers' - testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0' - testImplementation 'ch.qos.logback:logback-classic:1.3.8' - testImplementation 'org.assertj:assertj-core:3.24.2' + 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 { diff --git a/test-support/build.gradle b/test-support/build.gradle index 857f0fdf681..56fcbd98a92 100644 --- a/test-support/build.gradle +++ b/test-support/build.gradle @@ -1,5 +1,5 @@ dependencies { implementation 'junit:junit:4.13.2' - implementation 'org.slf4j:slf4j-api:1.7.36' - testImplementation 'org.assertj:assertj-core:3.25.2' + implementation 'org.slf4j:slf4j-api:2.0.17' + testImplementation 'org.assertj:assertj-core:3.27.4' }