From ebaaec87269a2af0741817da6525b943f517ef5d Mon Sep 17 00:00:00 2001 From: cdanger Date: Sat, 27 Aug 2022 22:08:59 +0200 Subject: [PATCH 1/5] - Fixes the issue of losing the root cause of a TimeoutException, especially SSL connection check errors when applying a HTTPS WaitStrategy (javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target). The exception was not propagated to the ContainerLaunchException, therefore not visible in the logs / final stacktrace. --- .../containers/wait/strategy/HttpWaitStrategy.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/testcontainers/containers/wait/strategy/HttpWaitStrategy.java b/core/src/main/java/org/testcontainers/containers/wait/strategy/HttpWaitStrategy.java index ccaef47a821..2400e3ae523 100644 --- a/core/src/main/java/org/testcontainers/containers/wait/strategy/HttpWaitStrategy.java +++ b/core/src/main/java/org/testcontainers/containers/wait/strategy/HttpWaitStrategy.java @@ -319,7 +319,8 @@ protected void waitUntilReady() { "Timed out waiting for URL to be accessible (%s should return HTTP %s)", uri, statusCodes.isEmpty() ? HttpURLConnection.HTTP_OK : statusCodes - ) + ), + e ); } } From fd0ebc0d5d4c3d8d89ba8ef23574bfb6d12638f6 Mon Sep 17 00:00:00 2001 From: cdanger Date: Thu, 1 Sep 2022 01:03:50 +0200 Subject: [PATCH 2/5] - Added test for pull request #5778 (fixing the code to propagate the cause of a HttpWaitStrategy failure in a ContainerLaunchException / TimeoutException, such as HTTPs check / certificate validation error): HttpWaitStrategyTest#testWaitUntilReadyWithTimeoutCausedBySslHandshakeError() --- .../strategy/AbstractWaitStrategyTest.java | 33 +++++++++++++++++++ .../wait/strategy/HttpWaitStrategyTest.java | 29 +++++++++++++++- .../https-wait-strategy-dockerfile/Dockerfile | 6 ++++ .../nginx-ssl.conf | 10 ++++++ 4 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 core/src/test/resources/https-wait-strategy-dockerfile/Dockerfile create mode 100644 core/src/test/resources/https-wait-strategy-dockerfile/nginx-ssl.conf diff --git a/core/src/test/java/org/testcontainers/junit/wait/strategy/AbstractWaitStrategyTest.java b/core/src/test/java/org/testcontainers/junit/wait/strategy/AbstractWaitStrategyTest.java index 23ad3802c1b..d1b6a167553 100644 --- a/core/src/test/java/org/testcontainers/junit/wait/strategy/AbstractWaitStrategyTest.java +++ b/core/src/test/java/org/testcontainers/junit/wait/strategy/AbstractWaitStrategyTest.java @@ -114,6 +114,39 @@ protected void waitUntilReadyAndTimeout(GenericContainer container) { .isInstanceOf(ContainerLaunchException.class); } + /** + * Expects that the WaitStrategy throws a {@link ContainerLaunchException} before the actual wait timeout, because of some connection error + * to the checked container with a listening port, e.g. TCP socket error, SSL/TLS handshake error, HTTP error, etc. + * + * @param container the container to start + * @param expectedTypeOfCause expected type of cause of the connection error, as part of the stack trace + */ + protected void waitUntilReadyAndTimeout( + GenericContainer container, + Class expectedTypeOfCause + ) { + if (expectedTypeOfCause == null) { + throw new IllegalArgumentException("expectedTypeOfCause undefined"); + } + // start() blocks until successful or timeout + assertThat(catchThrowable(container::start)) + .as("check the causes of the container launch exception") + .isInstanceOf(ContainerLaunchException.class) + .satisfies(throwable -> checkOneOfCausesIsExpectedType(throwable, expectedTypeOfCause)); + } + + private static void checkOneOfCausesIsExpectedType( + Throwable throwable, + Class expectedTypeOfCause + ) { + assertThat(throwable.getCause()) + .isNotNull() + .satisfiesAnyOf( + cause -> assertThat(cause).isInstanceOf(expectedTypeOfCause), + cause -> checkOneOfCausesIsExpectedType(cause, expectedTypeOfCause) + ); + } + /** * Expects that the WaitStrategy returns successfully after connection to a container with a listening port. * diff --git a/core/src/test/java/org/testcontainers/junit/wait/strategy/HttpWaitStrategyTest.java b/core/src/test/java/org/testcontainers/junit/wait/strategy/HttpWaitStrategyTest.java index 37bbe985bac..845937dd197 100644 --- a/core/src/test/java/org/testcontainers/junit/wait/strategy/HttpWaitStrategyTest.java +++ b/core/src/test/java/org/testcontainers/junit/wait/strategy/HttpWaitStrategyTest.java @@ -5,12 +5,15 @@ import org.rnorth.ducttape.RetryCountExceededException; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; +import org.testcontainers.images.builder.ImageFromDockerfile; import java.time.Duration; import java.util.HashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Predicate; +import javax.net.ssl.SSLHandshakeException; + import static org.assertj.core.api.Assertions.assertThat; /** @@ -200,7 +203,7 @@ public void testWaitUntilReadyWithSpecificPort() { } @Test - public void testWaitUntilReadyWithTimoutCausedByReadTimeout() { + public void testWaitUntilReadyWithTimeoutCausedByReadTimeout() { try ( GenericContainer container = startContainerWithCommand( createShellCommand("0 Connection Refused", GOOD_RESPONSE_BODY, 9090), @@ -212,6 +215,30 @@ public void testWaitUntilReadyWithTimoutCausedByReadTimeout() { } } + /** + * Test to validate fix from GitHub Pull Request #5778, i.e. when the container startup fails (ContainerLaunchException) before timeout for some reason, we are able to see the root cause of the error in the stack trace, e.g. in this case, a TLS certificate validation error during the TLS handshake test, because we are using a NGINX docker image with self-signed certificate created with the image, that is obviously not trusted. + * The exceptions we should see in the stacktrace ('/' means 'caused by'): ContainerLaunchException / TimeoutException / RuntimeException / SSLHandshakeException / ValidatorException (in sun.* package so not accessible) / SunCertPathBuilderException (in sun.* package so not accessible). + */ + @Test + public void testWaitUntilReadyWithTimeoutCausedBySslHandshakeError() { + try ( + GenericContainer container = new GenericContainer<>( + new ImageFromDockerfile() + .withFileFromClasspath("Dockerfile", "https-wait-strategy-dockerfile/Dockerfile") + .withFileFromClasspath("nginx-ssl.conf", "https-wait-strategy-dockerfile/nginx-ssl.conf") + ) + .withExposedPorts(8443) + .waitingFor( + createHttpWaitStrategy(ready) + .forPort(8443) + .usingTls() + .withStartupTimeout(Duration.ofMillis(WAIT_TIMEOUT_MILLIS)) + ) + ) { + waitUntilReadyAndTimeout(container, SSLHandshakeException.class); + } + } + /** * @param ready the AtomicBoolean on which to indicate success * @return the WaitStrategy under test diff --git a/core/src/test/resources/https-wait-strategy-dockerfile/Dockerfile b/core/src/test/resources/https-wait-strategy-dockerfile/Dockerfile new file mode 100644 index 00000000000..9a6d2245fb5 --- /dev/null +++ b/core/src/test/resources/https-wait-strategy-dockerfile/Dockerfile @@ -0,0 +1,6 @@ +FROM nginx:1.17-alpine + +# Create keypair and self-signed certificate for https test +RUN apk update && apk add bash openssl && openssl req -batch -x509 -nodes -days 365 -newkey rsa:2048 -subj "/CN=localhost" -keyout /etc/ssl/private/nginx-selfsigned.key -out /etc/ssl/certs/nginx-selfsigned.crt + +ADD nginx-ssl.conf /etc/nginx/conf.d/default.conf diff --git a/core/src/test/resources/https-wait-strategy-dockerfile/nginx-ssl.conf b/core/src/test/resources/https-wait-strategy-dockerfile/nginx-ssl.conf new file mode 100644 index 00000000000..021741f6b9e --- /dev/null +++ b/core/src/test/resources/https-wait-strategy-dockerfile/nginx-ssl.conf @@ -0,0 +1,10 @@ +# This configuration makes Nginx listen on port port 8443 +# In order to use this config, add this line to the Dockerfile to create the keypair and self-signed certificate: +# RUN apk update && apk add openssl && openssl req -batch -x509 -nodes -days 365 -newkey rsa:2048 -subj "/CN=localhost" -keyout /etc/ssl/private/nginx-selfsigned.key -out /etc/ssl/certs/nginx-selfsigned.crt + +server { + listen 8443 ssl; + server_name localhost; + ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt; + ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key; +} From e2d19e5b9876c5569b7bf37929524942e7d29ada Mon Sep 17 00:00:00 2001 From: Cyril Dangerville <1372580+cdanger@users.noreply.github.com> Date: Tue, 22 Nov 2022 21:41:21 +0100 Subject: [PATCH 3/5] Update core/src/test/java/org/testcontainers/junit/wait/strategy/HttpWaitStrategyTest.java Co-authored-by: Kevin Wittek --- .../junit/wait/strategy/HttpWaitStrategyTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/test/java/org/testcontainers/junit/wait/strategy/HttpWaitStrategyTest.java b/core/src/test/java/org/testcontainers/junit/wait/strategy/HttpWaitStrategyTest.java index 845937dd197..dddfc7dc5ef 100644 --- a/core/src/test/java/org/testcontainers/junit/wait/strategy/HttpWaitStrategyTest.java +++ b/core/src/test/java/org/testcontainers/junit/wait/strategy/HttpWaitStrategyTest.java @@ -235,7 +235,8 @@ public void testWaitUntilReadyWithTimeoutCausedBySslHandshakeError() { .withStartupTimeout(Duration.ofMillis(WAIT_TIMEOUT_MILLIS)) ) ) { - waitUntilReadyAndTimeout(container, SSLHandshakeException.class); + Throwable throwable = Assertions.catchThrowable(container::start); + assertThat(throwable).hasStackTraceContaining("javax.net.ssl.SSLHandshakeException"); } } From baff946c39995ce8327c62ee90e03f26f65aacf7 Mon Sep 17 00:00:00 2001 From: cdanger Date: Tue, 22 Nov 2022 22:07:39 +0100 Subject: [PATCH 4/5] - Based on kiview's suggestion, use `assertThat(throwable).hasStackTraceContaining("javax.net.ssl.SSLHandshakeException")` and remove all the custom code in `AbstractWaitStrategy` class. --- .../strategy/AbstractWaitStrategyTest.java | 33 ------------------- .../wait/strategy/HttpWaitStrategyTest.java | 13 ++++---- 2 files changed, 6 insertions(+), 40 deletions(-) diff --git a/core/src/test/java/org/testcontainers/junit/wait/strategy/AbstractWaitStrategyTest.java b/core/src/test/java/org/testcontainers/junit/wait/strategy/AbstractWaitStrategyTest.java index d1b6a167553..23ad3802c1b 100644 --- a/core/src/test/java/org/testcontainers/junit/wait/strategy/AbstractWaitStrategyTest.java +++ b/core/src/test/java/org/testcontainers/junit/wait/strategy/AbstractWaitStrategyTest.java @@ -114,39 +114,6 @@ protected void waitUntilReadyAndTimeout(GenericContainer container) { .isInstanceOf(ContainerLaunchException.class); } - /** - * Expects that the WaitStrategy throws a {@link ContainerLaunchException} before the actual wait timeout, because of some connection error - * to the checked container with a listening port, e.g. TCP socket error, SSL/TLS handshake error, HTTP error, etc. - * - * @param container the container to start - * @param expectedTypeOfCause expected type of cause of the connection error, as part of the stack trace - */ - protected void waitUntilReadyAndTimeout( - GenericContainer container, - Class expectedTypeOfCause - ) { - if (expectedTypeOfCause == null) { - throw new IllegalArgumentException("expectedTypeOfCause undefined"); - } - // start() blocks until successful or timeout - assertThat(catchThrowable(container::start)) - .as("check the causes of the container launch exception") - .isInstanceOf(ContainerLaunchException.class) - .satisfies(throwable -> checkOneOfCausesIsExpectedType(throwable, expectedTypeOfCause)); - } - - private static void checkOneOfCausesIsExpectedType( - Throwable throwable, - Class expectedTypeOfCause - ) { - assertThat(throwable.getCause()) - .isNotNull() - .satisfiesAnyOf( - cause -> assertThat(cause).isInstanceOf(expectedTypeOfCause), - cause -> checkOneOfCausesIsExpectedType(cause, expectedTypeOfCause) - ); - } - /** * Expects that the WaitStrategy returns successfully after connection to a container with a listening port. * diff --git a/core/src/test/java/org/testcontainers/junit/wait/strategy/HttpWaitStrategyTest.java b/core/src/test/java/org/testcontainers/junit/wait/strategy/HttpWaitStrategyTest.java index dddfc7dc5ef..124bb5d2b78 100644 --- a/core/src/test/java/org/testcontainers/junit/wait/strategy/HttpWaitStrategyTest.java +++ b/core/src/test/java/org/testcontainers/junit/wait/strategy/HttpWaitStrategyTest.java @@ -1,5 +1,11 @@ package org.testcontainers.junit.wait.strategy; +import java.time.Duration; +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; + +import org.assertj.core.api.Assertions; import org.jetbrains.annotations.NotNull; import org.junit.Test; import org.rnorth.ducttape.RetryCountExceededException; @@ -7,13 +13,6 @@ import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; import org.testcontainers.images.builder.ImageFromDockerfile; -import java.time.Duration; -import java.util.HashMap; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Predicate; - -import javax.net.ssl.SSLHandshakeException; - import static org.assertj.core.api.Assertions.assertThat; /** From 8ec25c10e0a3628c2c0b5cd02ceab771b47c6bbd Mon Sep 17 00:00:00 2001 From: cdanger Date: Wed, 23 Nov 2022 18:50:33 +0100 Subject: [PATCH 5/5] - Applied spotlessApply --- .../wait/strategy/HttpWaitStrategyTest.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/core/src/test/java/org/testcontainers/junit/wait/strategy/HttpWaitStrategyTest.java b/core/src/test/java/org/testcontainers/junit/wait/strategy/HttpWaitStrategyTest.java index 124bb5d2b78..d3ce006fb2e 100644 --- a/core/src/test/java/org/testcontainers/junit/wait/strategy/HttpWaitStrategyTest.java +++ b/core/src/test/java/org/testcontainers/junit/wait/strategy/HttpWaitStrategyTest.java @@ -1,10 +1,5 @@ package org.testcontainers.junit.wait.strategy; -import java.time.Duration; -import java.util.HashMap; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Predicate; - import org.assertj.core.api.Assertions; import org.jetbrains.annotations.NotNull; import org.junit.Test; @@ -13,6 +8,11 @@ import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; import org.testcontainers.images.builder.ImageFromDockerfile; +import java.time.Duration; +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; + import static org.assertj.core.api.Assertions.assertThat; /** @@ -215,9 +215,9 @@ public void testWaitUntilReadyWithTimeoutCausedByReadTimeout() { } /** - * Test to validate fix from GitHub Pull Request #5778, i.e. when the container startup fails (ContainerLaunchException) before timeout for some reason, we are able to see the root cause of the error in the stack trace, e.g. in this case, a TLS certificate validation error during the TLS handshake test, because we are using a NGINX docker image with self-signed certificate created with the image, that is obviously not trusted. - * The exceptions we should see in the stacktrace ('/' means 'caused by'): ContainerLaunchException / TimeoutException / RuntimeException / SSLHandshakeException / ValidatorException (in sun.* package so not accessible) / SunCertPathBuilderException (in sun.* package so not accessible). - */ + * Test to validate fix from GitHub Pull Request #5778, i.e. when the container startup fails (ContainerLaunchException) before timeout for some reason, we are able to see the root cause of the error in the stack trace, e.g. in this case, a TLS certificate validation error during the TLS handshake test, because we are using a NGINX docker image with self-signed certificate created with the image, that is obviously not trusted. + * The exceptions we should see in the stacktrace ('/' means 'caused by'): ContainerLaunchException / TimeoutException / RuntimeException / SSLHandshakeException / ValidatorException (in sun.* package so not accessible) / SunCertPathBuilderException (in sun.* package so not accessible). + */ @Test public void testWaitUntilReadyWithTimeoutCausedBySslHandshakeError() { try (