diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index 8d363cb978f..e41fe249d34 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -19,6 +19,7 @@ body:
- Cassandra
- ChromaDB
- Clickhouse
+ - Cloudflare
- CockroachDB
- Consul
- Couchbase
diff --git a/.github/ISSUE_TEMPLATE/enhancement.yaml b/.github/ISSUE_TEMPLATE/enhancement.yaml
index 6ee160c982b..1a7253b0123 100644
--- a/.github/ISSUE_TEMPLATE/enhancement.yaml
+++ b/.github/ISSUE_TEMPLATE/enhancement.yaml
@@ -19,6 +19,7 @@ body:
- Cassandra
- ChromaDB
- Clickhouse
+ - Cloudflare
- CockroachDB
- Consul
- Couchbase
diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml
index 3f8920b4059..5eda11949e1 100644
--- a/.github/ISSUE_TEMPLATE/feature.yaml
+++ b/.github/ISSUE_TEMPLATE/feature.yaml
@@ -19,6 +19,7 @@ body:
- Cassandra
- ChromaDB
- Clickhouse
+ - Cloudflare
- CockroachDB
- CrateDB
- Consul
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index bf9b145f2b3..3e77546ac15 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -61,6 +61,11 @@ updates:
schedule:
interval: "weekly"
open-pull-requests-limit: 10
+ - package-ecosystem: "gradle"
+ directory: "/modules/cloudflare"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
- package-ecosystem: "gradle"
directory: "/modules/cockroachdb"
schedule:
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 0fd1ec90d29..adb422ce83c 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -35,6 +35,10 @@
- changed-files:
- any-glob-to-any-file:
- modules/clickhouse/**/*
+"modules/cloudflare":
+ - changed-files:
+ - any-glob-to-any-file:
+ - modules/cloudflare/**/*
"modules/cockroachdb":
- changed-files:
- any-glob-to-any-file:
diff --git a/docs/modules/cloudflare.md b/docs/modules/cloudflare.md
new file mode 100644
index 00000000000..6d0077ed5c6
--- /dev/null
+++ b/docs/modules/cloudflare.md
@@ -0,0 +1,46 @@
+# Cloudflare Module
+
+Testcontainers module for Cloudflare Quick Tunnels(https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/) for exposing your app to the internet.
+
+This module is intended to be used for testing components that need to be exposed to public internet - for example to receive hooks from public cloud.
+Or to show your local state of the application to friends.
+
+## Usage example
+
+Start a Cloudflared container as follows:
+
+
+[Starting a Cloudflared Container](../../modules/cloudflare/src/test/java/org/testcontainers/cloudflare/CloudflaredContainerTest.java) inside_block:starting
+
+
+### Getting the public Url
+
+`Cloudflared` contaienr exposes a port on your host, via a [Quick tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/).
+To get the public url on which this port is available to the internet, call the `getPublicUrl` method.
+
+
+[Get the public Url](../../modules/cloudflare/src/test/java/org/testcontainers/cloudflare/CloudflaredContainerTest.java) inside_block:get_public_url
+
+
+## Known limitations
+
+!!! warning
+ * From the Cloudflare docs: "Quick Tunnels are subject to a hard limit on the number of concurrent requests that can be proxied at any point in time. Currently, this limit is 200 in-flight requests. If a Quick Tunnel hits this limit, the HTTP response will return a 429 status code."
+
+## Adding this module to your project dependencies
+
+Add the following dependency to your `pom.xml`/`build.gradle` file:
+
+=== "Gradle"
+ ```groovy
+ testImplementation "org.testcontainers:cloudflare:{{latest_version}}"
+ ```
+=== "Maven"
+ ```xml
+
+ org.testcontainers
+ cloudflare
+ {{latest_version}}
+ test
+
+ ```
diff --git a/modules/cloudflare/build.gradle b/modules/cloudflare/build.gradle
new file mode 100644
index 00000000000..4a1d08c7c8b
--- /dev/null
+++ b/modules/cloudflare/build.gradle
@@ -0,0 +1,7 @@
+description = "Testcontainers :: Cloudflare"
+
+dependencies {
+ api project(":testcontainers")
+
+ testImplementation 'org.assertj:assertj-core:3.25.2'
+}
diff --git a/modules/cloudflare/src/main/java/org/testcontainers/cloudflare/CloudflaredContainer.java b/modules/cloudflare/src/main/java/org/testcontainers/cloudflare/CloudflaredContainer.java
new file mode 100644
index 00000000000..edc30b58295
--- /dev/null
+++ b/modules/cloudflare/src/main/java/org/testcontainers/cloudflare/CloudflaredContainer.java
@@ -0,0 +1,40 @@
+package org.testcontainers.cloudflare;
+
+import org.testcontainers.Testcontainers;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+public class CloudflaredContainer extends GenericContainer {
+
+ private String publicUrl;
+
+ public CloudflaredContainer(DockerImageName dockerImageName, int port) {
+ super(dockerImageName);
+ dockerImageName.assertCompatibleWith(DockerImageName.parse("cloudflare/cloudflared"));
+ withAccessToHost(true);
+ Testcontainers.exposeHostPorts(port);
+ withCommand("tunnel", "--url", String.format("http://host.testcontainers.internal:%d", port));
+ waitingFor(Wait.forLogMessage(".*Registered tunnel connection.*", 1));
+ }
+
+ public String getPublicUrl() {
+ if (null != publicUrl) {
+ return publicUrl;
+ }
+ String logs = getLogs();
+ String[] split = logs.split(String.format("%n"));
+ boolean found = false;
+ for (int i = 0; i < split.length; i++) {
+ String currentLine = split[i];
+ if (currentLine.contains("Your quick Tunnel has been created")) {
+ found = true;
+ continue;
+ }
+ if (found) {
+ return publicUrl = currentLine.substring(currentLine.indexOf("http"), currentLine.indexOf(".com") + 4);
+ }
+ }
+ throw new IllegalStateException("Didn't find public url in logs. Has container started?");
+ }
+}
diff --git a/modules/cloudflare/src/test/java/org/testcontainers/cloudflare/CloudflaredContainerTest.java b/modules/cloudflare/src/test/java/org/testcontainers/cloudflare/CloudflaredContainerTest.java
new file mode 100644
index 00000000000..b0fbeecfa06
--- /dev/null
+++ b/modules/cloudflare/src/test/java/org/testcontainers/cloudflare/CloudflaredContainerTest.java
@@ -0,0 +1,62 @@
+package org.testcontainers.cloudflare;
+
+import org.junit.Test;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
+import org.testcontainers.utility.DockerImageName;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URL;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CloudflaredContainerTest {
+
+ @Test
+ public void shouldStartAndTunnelToHelloWorld() throws IOException {
+ try (
+ GenericContainer> helloworld = new GenericContainer<>(
+ DockerImageName.parse("testcontainers/helloworld:1.1.0")
+ )
+ .withNetworkAliases("helloworld")
+ .withExposedPorts(8080, 8081)
+ .waitingFor(new HttpWaitStrategy())
+ ) {
+ helloworld.start();
+
+ try (
+ // starting {
+ CloudflaredContainer cloudflare = new CloudflaredContainer(
+ DockerImageName.parse("cloudflare/cloudflared:latest"),
+ helloworld.getFirstMappedPort()
+ );
+ //
+ ) {
+ cloudflare.start();
+ // get_public_url {
+ String url = cloudflare.getPublicUrl();
+ // }
+
+ assertThat(url).as("Public url contains 'cloudflare'").contains("cloudflare");
+ String body = readUrl(url);
+
+ assertThat(body.trim()).as("the index page contains the title 'Hello world'").contains("Hello world");
+ }
+ }
+ }
+
+ private String readUrl(String url) throws IOException {
+ BufferedReader in = new BufferedReader(new InputStreamReader(new URL(url).openStream()));
+
+ StringBuilder sb = new StringBuilder();
+ String inputLine = null;
+ while ((inputLine = in.readLine()) != null) {
+ sb.append(inputLine);
+ }
+ in.close();
+
+ return sb.toString();
+ }
+}
diff --git a/modules/cloudflare/src/test/resources/logback-test.xml b/modules/cloudflare/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..83ef7a1a3ef
--- /dev/null
+++ b/modules/cloudflare/src/test/resources/logback-test.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n
+
+
+
+
+
+
+
+
+