testcontainers / testcontainers-java

Testcontainers is a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container.
https://testcontainers.org
MIT License
8.05k stars 1.66k forks source link

[Bug]: Unable to build from a Dockerfile loaded from the classpath of a Jar. #9423

Open ml-james opened 1 month ago

ml-james commented 1 month ago

Module

Core

Testcontainers version

1.20.2

Using the latest Testcontainers version?

Yes

Host OS

Linux

Host Arch

x86

Docker version

[jamesm@wt1devps05 solana4j]$ docker --version Docker version 26.1.4, build 5650f9b

What happened?

Unable to build from a Dockerfile read from a Jar.

In fact, loading files from the classpath from jars in general looks a bit broken to me. I raised an issue a few days ago that became unwieldy, so I tried to recreate the issue in as simple a way as possible. That previous issue I closed.

I have created a unit test that proves the problem. Just apply the patch in the "Additional Information" section.

Question I have simply is am I doing something wrong, or is this indeed a bug, or is this simply not supported? It certainly looks like the code is trying quite hard to support what I'm trying to do.

I'd be happy to devote some time to raise a PR if you agree that this is not expected behaviour.

Thanks.

Relevant log output

Caused by: com.github.dockerjava.api.exception.DockerClientException: Failed to read build context directory: /tmp at org.testcontainers.shaded.com.github.dockerjava.core.dockerfile.Dockerfile$ScannedResult.addFilesInDirectory(Dockerfile.java:201) at org.testcontainers.shaded.com.github.dockerjava.core.dockerfile.Dockerfile$ScannedResult.addFilesInDirectory(Dockerfile.java:207) at org.testcontainers.shaded.com.github.dockerjava.core.dockerfile.Dockerfile$ScannedResult.(Dockerfile.java:186) at org.testcontainers.shaded.com.github.dockerjava.core.dockerfile.Dockerfile.parse(Dockerfile.java:111) at org.testcontainers.shaded.com.github.dockerjava.core.command.BuildImageCmdImpl.withDockerfile(BuildImageCmdImpl.java:344) at org.testcontainers.shaded.com.github.dockerjava.core.command.BuildImageCmdImpl.withDockerfile(BuildImageCmdImpl.java:22) at org.testcontainers.images.builder.ImageFromDockerfile.lambda$configure$0(ImageFromDockerfile.java:177) at java.base/java.util.Optional.ifPresent(Optional.java:178) at org.testcontainers.images.builder.ImageFromDockerfile.configure(ImageFromDockerfile.java:176) at org.testcontainers.images.builder.ImageFromDockerfile.resolve(ImageFromDockerfile.java:124) at org.testcontainers.images.builder.ImageFromDockerfile.resolve(ImageFromDockerfile.java:43) at org.testcontainers.utility.LazyFuture.getResolvedValue(LazyFuture.java:20) at org.testcontainers.utility.LazyFuture.get(LazyFuture.java:41) at org.testcontainers.shaded.com.google.common.util.concurrent.Futures$1.get(Futures.java:538) at org.testcontainers.images.RemoteDockerImage.getImageName(RemoteDockerImage.java:172) at org.testcontainers.images.RemoteDockerImage.resolve(RemoteDockerImage.java:76) at org.testcontainers.images.RemoteDockerImage.resolve(RemoteDockerImage.java:35) at org.testcontainers.utility.LazyFuture.getResolvedValue(LazyFuture.java:20) at org.testcontainers.utility.LazyFuture.get(LazyFuture.java:41) at org.testcontainers.containers.GenericContainer.getDockerImageName(GenericContainer.java:1354) ... 54 more

Failed to read build context directory: /tmp com.github.dockerjava.api.exception.DockerClientException: Failed to read build context directory: /tmp at app//org.testcontainers.shaded.com.github.dockerjava.core.dockerfile.Dockerfile$ScannedResult.addFilesInDirectory(Dockerfile.java:201) at app//org.testcontainers.shaded.com.github.dockerjava.core.dockerfile.Dockerfile$ScannedResult.addFilesInDirectory(Dockerfile.java:207) at app//org.testcontainers.shaded.com.github.dockerjava.core.dockerfile.Dockerfile$ScannedResult.(Dockerfile.java:186) at app//org.testcontainers.shaded.com.github.dockerjava.core.dockerfile.Dockerfile.parse(Dockerfile.java:111) at app//org.testcontainers.shaded.com.github.dockerjava.core.command.BuildImageCmdImpl.withDockerfile(BuildImageCmdImpl.java:344) at app//org.testcontainers.shaded.com.github.dockerjava.core.command.BuildImageCmdImpl.withDockerfile(BuildImageCmdImpl.java:22) at app//org.testcontainers.images.builder.ImageFromDockerfile.lambda$configure$0(ImageFromDockerfile.java:177) at java.base@17.0.11/java.util.Optional.ifPresent(Optional.java:178) at app//org.testcontainers.images.builder.ImageFromDockerfile.configure(ImageFromDockerfile.java:176) at app//org.testcontainers.images.builder.ImageFromDockerfile.resolve(ImageFromDockerfile.java:124) at app//org.testcontainers.images.builder.ImageFromDockerfile.resolve(ImageFromDockerfile.java:43) at app//org.testcontainers.utility.LazyFuture.getResolvedValue(LazyFuture.java:20) at app//org.testcontainers.utility.LazyFuture.get(LazyFuture.java:41) at app//org.testcontainers.shaded.com.google.common.util.concurrent.Futures$1.get(Futures.java:538) at app//org.testcontainers.images.RemoteDockerImage.getImageName(RemoteDockerImage.java:172) at app//org.testcontainers.images.RemoteDockerImage.resolve(RemoteDockerImage.java:76) at app//org.testcontainers.images.RemoteDockerImage.resolve(RemoteDockerImage.java:35) at app//org.testcontainers.utility.LazyFuture.getResolvedValue(LazyFuture.java:20) at app//org.testcontainers.utility.LazyFuture.get(LazyFuture.java:41) at app//org.testcontainers.containers.GenericContainer.getDockerImageName(GenericContainer.java:1354) at app//org.testcontainers.containers.GenericContainer.doStart(GenericContainer.java:351) at app//org.testcontainers.containers.GenericContainer.start(GenericContainer.java:322) at app//org.testcontainers.images.builder.DockerfileBuildTest.performTest(DockerfileBuildTest.java:96) at java.base@17.0.11/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base@17.0.11/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) at java.base@17.0.11/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base@17.0.11/java.lang.reflect.Method.invoke(Method.java:568) at app//org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) at app//org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at app//org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) at app//org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at app//org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) at app//org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) at app//org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) at app//org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) at app//org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) at app//org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) at app//org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) at app//org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) at app//org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) at app//org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) at app//org.junit.runners.ParentRunner.run(ParentRunner.java:413) at app//org.junit.runners.Suite.runChild(Suite.java:128) at app//org.junit.runners.Suite.runChild(Suite.java:27) at app//org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) at app//org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) at app//org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) at app//org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) at app//org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) at app//org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) at app//org.junit.runners.ParentRunner.run(ParentRunner.java:413) at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.runTestClass(JUnitTestClassExecutor.java:112) at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:58) at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:40) at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestClassProcessor.processTestClass(AbstractJUnitTestClassProcessor.java:60) at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:52) at java.base@17.0.11/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base@17.0.11/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) at java.base@17.0.11/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base@17.0.11/java.lang.reflect.Method.invoke(Method.java:568) at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36) at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24) at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94) at jdk.proxy1/jdk.proxy1.$Proxy2.processTestClass(Unknown Source) at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:176) at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129) at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100) at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60) at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:113) at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:65) at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)

Additional Information

Index: core/src/test/java/org/testcontainers/images/builder/DockerfileBuildTest.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/test/java/org/testcontainers/images/builder/DockerfileBuildTest.java b/core/src/test/java/org/testcontainers/images/builder/DockerfileBuildTest.java
--- a/core/src/test/java/org/testcontainers/images/builder/DockerfileBuildTest.java (revision cd29df97aa06e744ed4d8c17ca33b7983ef7f338)
+++ b/core/src/test/java/org/testcontainers/images/builder/DockerfileBuildTest.java (date 1729547678601)
@@ -5,6 +5,7 @@
 import org.junit.runners.Parameterized;
 import org.testcontainers.containers.GenericContainer;
 import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy;
+import org.testcontainers.utility.MountableFile;

 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -70,6 +71,13 @@
                     .withFileFromPath(".", RESOURCE_PATH)
                     .withDockerfile(RESOURCE_PATH.resolve("Dockerfile-alt")),
             },
+            // Dockerfile build using dockerfile from resources
+            new Object[]{
+                "test4567",
+                new ImageFromDockerfile().withDockerfile(
+                        Paths.get(MountableFile.forClasspathResource("/recursive/Dockerfile").getFilesystemPath())
+                    ),
+            },
         };
     }

Index: core/testlib/recursive/Dockerfile
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/testlib/recursive/Dockerfile b/core/testlib/recursive/Dockerfile
new file mode 100644
--- /dev/null   (date 1729547092581)
+++ b/core/testlib/recursive/Dockerfile (date 1729547092581)
@@ -0,0 +1,1 @@
+FROM postgres

Run core/testlib/create_fakejar.sh and then run the test in the above patch, should replicate the issue.

ml-james commented 1 month ago

The problem arises because for some reason in MountableFile the code strips the internal path from the resource within the jar. This leads to the file being written directly at /tmp/.testcontainers-tmp-8883751548680426941. When that file path gets sent to docker-java it fails, because docker-java will recursively search for files within the parent directory, in this case /tmp. Absolutely anything could be in /tmp, for me I have things owned by root which throws the exception shown.

The code that does that is here, where String destinationName = entry.getName().replaceFirst(fromRoot, ""); is the line of interest, where the fromRoot is the internal path in question, i.e. the path of the resource within the provided jar. Stripping this will leave destinationName as "" and therefore the contents of the resource is just written directly to the toRoot path, which is the /tmp/.testcontainers-tmp-8883751548680426941 file, where of course .testcontainers-tmp-8883751548680426941 is generated each time.

    @SuppressWarnings("ResultOfMethodCallIgnored")
    private void copyFromJarToLocation(
        final JarFile jarFile,
        final JarEntry entry,
        final String fromRoot,
        final File toRoot
    ) throws IOException {
        String destinationName = entry.getName().replaceFirst(fromRoot, "");
        File newFile = new File(toRoot, destinationName);

        log.debug("Copying resource {} from JAR file {}", fromRoot, jarFile.getName());

        if (!entry.isDirectory()) {
            // Create parent directories
            Path parent = newFile.getAbsoluteFile().toPath().getParent();
            parent.toFile().mkdirs();
            newFile.deleteOnExit();

            try (InputStream is = jarFile.getInputStream(entry)) {
                Files.copy(is, newFile.toPath());
            } catch (IOException e) {
                log.error(
                    "Failed to extract classpath resource " + entry.getName() + " from JAR file " + jarFile.getName(),
                    e
                );
                throw e;
            }
        }
    }

I'm not sure why we're stripping the internal path from the file path when we copy to the temp directory. My proposed solution is as follows, which allows my test to pass.

Index: core/src/main/java/org/testcontainers/utility/MountableFile.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/main/java/org/testcontainers/utility/MountableFile.java b/core/src/main/java/org/testcontainers/utility/MountableFile.java
--- a/core/src/main/java/org/testcontainers/utility/MountableFile.java  (revision cd29df97aa06e744ed4d8c17ca33b7983ef7f338)
+++ b/core/src/main/java/org/testcontainers/utility/MountableFile.java  (date 1729554209095)
@@ -264,7 +264,7 @@
         deleteOnExit(tmpLocation.toPath());

         try {
-            return tmpLocation.getCanonicalPath();
+            return tmpLocation.getCanonicalPath() + "/" + internalPath;
         } catch (IOException e) {
             throw new IllegalStateException(e);
         }
@@ -288,7 +288,7 @@
         final String fromRoot,
         final File toRoot
     ) throws IOException {
-        String destinationName = entry.getName().replaceFirst(fromRoot, "");
+        String destinationName = entry.getName();
         File newFile = new File(toRoot, destinationName);

         log.debug("Copying resource {} from JAR file {}", fromRoot, jarFile.getName());

Let me know if this looks sensible and I can raise a PR.