Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
ml-james opened this issue Oct 21, 2024 · 1 comment
Labels

Comments

@ml-james
Copy link

ml-james commented Oct 21, 2024

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 ml-james changed the title [Bug]: [Bug]: Unable to build from a Dockerfile loaded from the classpath of a Jar. Oct 21, 2024
@ml-james
Copy link
Author

ml-james commented Oct 21, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant