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 + + + + + + + + +