Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion core/build.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator

apply plugin: 'com.gradleup.shadow'

description = "Testcontainers Core"
Expand Down Expand Up @@ -26,9 +28,43 @@ shadowJar {
'META-INF/proguard/',
'META-INF/versions/*/module-info.class',
'META-INF/services/java.security.Provider',
'META-INF/DEPENDENCIES',
'META-INF/AL2.0',
'META-INF/LGPL2.1',
].each { exclude(it) }
}

afterEvaluate {
// gradle/shading.gradle replaces shadowJar.relocators within its own afterEvaluate
// block, so these must be registered afterwards. The auto-generated relocations
// only cover class packages; the httpclient5/httpcore5 resources (version.properties)
// would otherwise stay behind at their original paths.
tasks.shadowJar.relocate('org.apache.hc.client5', 'org.testcontainers.shaded.org.apache.hc.client5') {
exclude 'org.apache.hc.client5.http.wire'
exclude 'org.apache.hc.client5.http.headers'
}
tasks.shadowJar.relocate('org.apache.hc.core5', 'org.testcontainers.shaded.org.apache.hc.core5')

// Keep the Apache HttpClient wire/headers logger names out of the `org.testcontainers`
// hierarchy. The logger names are plain string constants inside the shaded classes
// ("org.apache.hc.client5.http.wire" / "org.apache.hc.client5.http.headers") and shadow
// relocates them along with the code. If they landed under
// `org.testcontainers.shaded...`, they would inherit the level of the `org.testcontainers`
// logger: raising it to DEBUG (the documented troubleshooting switch) would silently turn
// on full wire/header logging of the Docker API traffic, including registry credentials.
// With the original logger names preserved, wire logging stays off by default and only
// activates when the user explicitly enables that exact logger (see
// docs/supported_docker_environment/logging_config.md). The excludes above are not enough
// on their own: the auto-generated per-package relocator for `org.apache.hc.client5.http`
// (gradle/shading.gradle) would still match the strings, so it must skip them as well.
tasks.shadowJar.relocators
.findAll { it instanceof SimpleRelocator && it.pattern == 'org.apache.hc.client5.http' }
.each { relocator ->
relocator.exclude('org.apache.hc.client5.http.wire')
relocator.exclude('org.apache.hc.client5.http.headers')
}
}

task jarFileTest(type: Test) {
useJUnitPlatform()
testClassesDirs = sourceSets.jarFileTest.output.classesDirs
Expand Down Expand Up @@ -84,12 +120,28 @@ dependencies {
shaded platform('com.github.docker-java:docker-java-bom:3.7.1')

api "com.github.docker-java:docker-java-api"
api 'com.github.docker-java:docker-java-transport'

shaded('com.github.docker-java:docker-java-core') {
exclude group: 'com.google.guava', module: 'guava'
}

api 'com.github.docker-java:docker-java-transport-zerodep'
// Embed the HTTP transport ourselves instead of depending on
// com.github.docker-java:docker-java-transport-zerodep, whose jar bundles
// httpclient5 5.5.1 / httpcore5(-h2) 5.3.6, affected by CVE-2026-54428
// (see https://github.com/testcontainers/testcontainers-java/issues/11973).
// httpclient5 5.6.4 brings httpcore5 5.4.3, the first fixed version.
shaded 'com.github.docker-java:docker-java-transport-httpclient5'
shaded 'org.apache.httpcomponents.client5:httpclient5:5.6.4'

// docker-java-transport-httpclient5 depends on JNA, which used to reach the api
// configuration transitively via docker-java-transport-zerodep. Declared as an api
// dependency here, JNA is excluded from the shaded jar and left unrelocated: shaded
// (relocated) JNA classes cannot bind the native jnidispatch library, whose JNI
// symbols and embedded resource path stay at com/sun/jna/... - relocating them
// breaks DockerClientProviderStrategy resolution with an UnsatisfiedLinkError in
// RootlessDockerClientProviderStrategy (ci/circleci: minimal_core failure).
api 'net.java.dev.jna:jna:5.18.1'

shaded 'com.google.guava:guava:33.3.1-jre'
shaded "org.yaml:snakeyaml:2.6"
Expand Down Expand Up @@ -129,6 +181,10 @@ dependencies {
tasks.generatePomFileForMavenJavaPublication.finalizedBy(
tasks.register('checkPOMdependencies', org.testcontainers.build.ComparePOMWithLatestReleasedTask) {
ignore = [
// previously pulled in transitively by docker-java-transport-zerodep (#11973)
'com.github.docker-java:docker-java-transport',
// same for JNA, previously pulled in transitively by docker-java-transport-zerodep (#11973)
'net.java.dev.jna:jna',
]
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;

Expand All @@ -15,7 +19,9 @@ class JarFileShadingTest extends AbstractJarFileTest {
void testPackages() throws Exception {
assertThatFileList(root).containsOnly("org", "META-INF");

assertThatFileList(root.resolve("org")).containsOnly("testcontainers");
// "publicsuffix" is the data-only resource directory of the shaded httpclient5
// (effective_tld_names.dat), not relocated, just like in docker-java's zerodep jar
assertThatFileList(root.resolve("org")).containsOnly("testcontainers", "publicsuffix");
}

@Test
Expand All @@ -38,6 +44,51 @@ void testMetaInfServices() throws Exception {
.allMatch(it -> it.startsWith("org.testcontainers."));
}

@Test
void testHttpClientWireLoggersAreNotRelocated() throws Exception {
// The shaded Apache HttpClient must send wire/header output to the plain
// "org.apache.hc.client5.http.wire" / "org.apache.hc.client5.http.headers" loggers.
// The logger names are plain string constants, and if they were relocated along with
// the code (to "org.testcontainers.shaded..."), the loggers would inherit the level of
// the `org.testcontainers` logger: raising it to DEBUG (the documented troubleshooting
// switch in docs/supported_docker_environment/logging_config.md) would then print the
// full Docker API traffic, including registry credentials. Wire logging must stay
// opt-in on the exact, unrelocated logger names.
Path shadedHttpClientClasses = root
.resolve("org/testcontainers/shaded/org/apache/hc")
.toAbsolutePath()
.normalize();

try (Stream<Path> entries = Files.walk(shadedHttpClientClasses)) {
List<Path> shadedClasses = entries
.filter(path -> path.toString().endsWith(".class"))
.collect(Collectors.toList());

assertThat(shadedClasses).isNotEmpty();

for (Path shadedClass : shadedClasses) {
String constantPool = new String(Files.readAllBytes(shadedClass), StandardCharsets.ISO_8859_1);
assertThat(constantPool)
.as("constant pool of %s", shadedClass)
.doesNotContain("org.testcontainers.shaded.org.apache.hc.client5.http.wire")
.doesNotContain("org.testcontainers.shaded.org.apache.hc.client5.http.headers");
}
}

// the wire/header logger names must keep their original, opt-in values
String connectionClass = new String(
Files.readAllBytes(
root.resolve(
"org/testcontainers/shaded/org/apache/hc/client5/http/impl/io/DefaultManagedHttpClientConnection.class"
)
),
StandardCharsets.ISO_8859_1
);
assertThat(connectionClass)
.contains("org.apache.hc.client5.http.wire")
.contains("org.apache.hc.client5.http.headers");
}

private ListAssert<String> assertThatFileList(Path path) throws IOException {
return (ListAssert) assertThat(Files.list(path))
.extracting(Path::getFileName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientImpl;
import com.github.dockerjava.core.RemoteApiVersion;
import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
import com.github.dockerjava.transport.DockerHttpClient;
import com.github.dockerjava.transport.NamedPipeSocket;
import com.github.dockerjava.transport.SSLConfig;
import com.github.dockerjava.transport.UnixSocket;
import com.github.dockerjava.zerodep.ZerodepDockerHttpClient;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Throwables;
import lombok.Getter;
Expand Down Expand Up @@ -392,7 +392,7 @@ public static DockerClient getClientForConfig(TransportConfig transportConfig) {
switch (transportType) {
case "httpclient5":
dockerHttpClient =
new ZerodepDockerHttpClient.Builder()
new ApacheDockerHttpClient.Builder()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
.dockerHost(transportConfig.getDockerHost())
.sslConfig(transportConfig.getSslConfig())
.build();
Expand Down
4 changes: 4 additions & 0 deletions core/src/test/resources/logback-test.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@
<logger name="org.zeroturnaround.exec" level="WARN"/>
<logger name="org.testcontainers.shaded" level="WARN"/>

<!-- Docker API wire/header logging is opt-in only (the logs include credentials) -->
<logger name="org.apache.hc.client5.http.wire" level="OFF"/>
<logger name="org.apache.hc.client5.http.headers" level="OFF"/>

</configuration>
29 changes: 27 additions & 2 deletions docs/supported_docker_environment/logging_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@ should be included in your classpath to show a reasonable level of log output:
<!-- The following logger can be used for containers logs since 1.18.0 -->
<logger name="tc" level="INFO"/>
<logger name="com.github.dockerjava" level="WARN"/>
<logger name="com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire" level="OFF"/>
<!--
Testcontainers talks to the Docker daemon over a shaded copy of Apache HttpClient.
These loggers print the full HTTP traffic, including credentials, and must stay OFF
unless explicitly enabled. The names cover both the shaded copy bundled with
Testcontainers and any plain (unshaded) Apache HttpClient 5.x on your classpath.
-->
<logger name="org.apache.hc.client5.http.wire" level="OFF"/>
<logger name="org.apache.hc.client5.http.headers" level="OFF"/>
</configuration>
```

Expand All @@ -30,4 +37,22 @@ In order to troubleshoot issues with Testcontainers, increase the logging level
<logger name="org.testcontainers" level="DEBUG"/>
```

Avoid changing the root logger's level to `DEBUG`, because this turns on debug logging for every package whose level isn't explicitly configured here, resulting in a large amount of log data.
Raising `org.testcontainers` (or `com.github.dockerjava`) to `DEBUG` does not turn on HTTP wire logging for the
communication with the Docker daemon. That logging is off by default and opt-in only, because the wire logs include
complete HTTP request and response headers and bodies, including registry credentials (e.g. the Base64-encoded
`X-Registry-Auth` header used for image pulls). To enable it temporarily for troubleshooting, explicitly set the exact
logger names to `DEBUG`:

```xml
<logger name="org.apache.hc.client5.http.wire" level="DEBUG"/>
<!-- or, for request/response headers only: -->
<logger name="org.apache.hc.client5.http.headers" level="DEBUG"/>
```

The same logger names apply whether Testcontainers is using its shaded copy of Apache HttpClient (from the published
jar) or a plain Apache HttpClient 5.x (e.g. when running from a source classpath), and they also control wire logging
for other Apache HttpClient 5.x usage in your application.

Avoid changing the root logger's level to `DEBUG`, because this turns on debug logging for every package whose level
isn't explicitly configured here — resulting in a large amount of log data and, in particular, in wire logging of the
Docker API communication (see above), which includes credentials.
Loading