From 6351b1f45d6afadf05c3eca37a9121f0b77a7b8f Mon Sep 17 00:00:00 2001 From: Richard North Date: Mon, 10 Apr 2017 20:39:08 +0100 Subject: [PATCH] Introduce an abstraction over files and classpath resources. This encapsulates all the complexity of generating a path that the Docker daemon is about to create a volume mount for. This should resolve a general problem with spaces in paths, which was seen in one particular form as #263. Use recursive copy for docker context TAR to allow contents of directories to be used in Dockerfiles and Docker Compose contexts --- core/pom.xml | 14 ++ .../containers/DockerComposeContainer.java | 18 +- .../images/builder/ImageFromDockerfile.java | 13 +- .../images/builder/Transferable.java | 32 +++- .../builder/traits/DockerfileTrait.java | 11 +- .../images/builder/traits/FilesTrait.java | 33 +--- .../images/builder/traits/StringsTrait.java | 15 +- .../testcontainers/utility/MountableFile.java | 166 +++++++++++++----- .../testcontainers/junit/DockerfileTest.java | 14 +- .../utility/DirectoryTarResourceTest.java | 71 ++++++++ .../utility/MountableFileTest.java | 12 +- .../test/resources/test-recursive-file.txt | 1 + core/testlib/META-INF/dummy_unique_name.txt | 1 + core/testlib/README.md | 3 + core/testlib/create_fakejar.sh | 5 + core/testlib/recursive/dir/content.txt | 1 + .../repo/fakejar/fakejar/0/fakejar-0.jar | Bin 0 -> 949 bytes .../repo/fakejar/fakejar/0/fakejar-0.pom | 9 + .../fakejar/fakejar/maven-metadata-local.xml | 12 ++ 19 files changed, 312 insertions(+), 119 deletions(-) create mode 100644 core/src/test/java/org/testcontainers/utility/DirectoryTarResourceTest.java create mode 100644 core/src/test/resources/test-recursive-file.txt create mode 100644 core/testlib/META-INF/dummy_unique_name.txt create mode 100644 core/testlib/README.md create mode 100755 core/testlib/create_fakejar.sh create mode 100644 core/testlib/recursive/dir/content.txt create mode 100644 core/testlib/repo/fakejar/fakejar/0/fakejar-0.jar create mode 100644 core/testlib/repo/fakejar/fakejar/0/fakejar-0.pom create mode 100644 core/testlib/repo/fakejar/fakejar/maven-metadata-local.xml diff --git a/core/pom.xml b/core/pom.xml index 3469815e7ae..9edcaefa9b2 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -82,6 +82,13 @@ + + + fakejar + fakejar + 0 + test + @@ -232,4 +239,11 @@ + + + + testlib + file://${project.basedir}/testlib/repo + + diff --git a/core/src/main/java/org/testcontainers/containers/DockerComposeContainer.java b/core/src/main/java/org/testcontainers/containers/DockerComposeContainer.java index ecd23770db6..b69e2abcf8f 100644 --- a/core/src/main/java/org/testcontainers/containers/DockerComposeContainer.java +++ b/core/src/main/java/org/testcontainers/containers/DockerComposeContainer.java @@ -8,7 +8,6 @@ import com.google.common.base.Splitter; import com.google.common.collect.Maps; import com.google.common.util.concurrent.Uninterruptibles; -import org.apache.commons.lang.SystemUtils; import org.junit.runner.Description; import org.rnorth.ducttape.ratelimits.RateLimiter; import org.rnorth.ducttape.ratelimits.RateLimiterBuilder; @@ -32,6 +31,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.toList; import static org.testcontainers.containers.BindMode.READ_ONLY; import static org.testcontainers.containers.BindMode.READ_WRITE; @@ -196,7 +196,7 @@ private List listChildContainers() { .exec().stream() .filter(container -> Arrays.stream(container.getNames()).anyMatch(name -> name.startsWith("/" + identifier))) - .collect(Collectors.toList()); + .collect(toList()); } private void startAmbassadorContainers(Profiler profiler) { @@ -394,15 +394,13 @@ public ContainerisedDockerCompose(List composeFiles, String identifier) { // Map the docker compose file into the container final File dockerComposeBaseFile = composeFiles.get(0); final String pwd = dockerComposeBaseFile.getAbsoluteFile().getParentFile().getAbsolutePath(); - final String containerPwd; - if (SystemUtils.IS_OS_WINDOWS) { - containerPwd = PathUtils.createMinGWPath(pwd).substring(1); - } else { - containerPwd = pwd; - } + final String containerPwd = MountableFile.forHostPath(pwd).getResolvedPath(); - final List absoluteDockerComposeFiles = composeFiles.stream().map( - file -> containerPwd + "/" + file.getAbsoluteFile().getName()).collect(Collectors.toList()); + final List absoluteDockerComposeFiles = composeFiles.stream() + .map(File::getAbsolutePath) + .map(MountableFile::forHostPath) + .map(MountableFile::getResolvedPath) + .collect(toList()); final String composeFileEnvVariableValue = Joiner.on(File.pathSeparator).join(absoluteDockerComposeFiles); logger().debug("Set env COMPOSE_FILE={}", composeFileEnvVariableValue); addEnv(ENV_COMPOSE_FILE, composeFileEnvVariableValue); diff --git a/core/src/main/java/org/testcontainers/images/builder/ImageFromDockerfile.java b/core/src/main/java/org/testcontainers/images/builder/ImageFromDockerfile.java index e51e6d003af..6d9427b61f3 100644 --- a/core/src/main/java/org/testcontainers/images/builder/ImageFromDockerfile.java +++ b/core/src/main/java/org/testcontainers/images/builder/ImageFromDockerfile.java @@ -9,7 +9,6 @@ import lombok.Cleanup; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; @@ -128,16 +127,12 @@ public void onNext(BuildResponseItem item) { profiler.start("Send context as TAR"); try (TarArchiveOutputStream tarArchive = new TarArchiveOutputStream(new GZIPOutputStream(out))) { + tarArchive.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); + for (Map.Entry entry : transferables.entrySet()) { - TarArchiveEntry tarEntry = new TarArchiveEntry(entry.getKey()); Transferable transferable = entry.getValue(); - tarEntry.setSize(transferable.getSize()); - tarEntry.setMode(transferable.getFileMode()); - - tarArchive.putArchiveEntry(tarEntry); - transferable.transferTo(tarArchive); - tarArchive.closeArchiveEntry(); - + final String destination = entry.getKey(); + transferable.transferTo(tarArchive, destination); } tarArchive.finish(); } diff --git a/core/src/main/java/org/testcontainers/images/builder/Transferable.java b/core/src/main/java/org/testcontainers/images/builder/Transferable.java index eb245172596..b68ae95ac43 100644 --- a/core/src/main/java/org/testcontainers/images/builder/Transferable.java +++ b/core/src/main/java/org/testcontainers/images/builder/Transferable.java @@ -1,16 +1,21 @@ package org.testcontainers.images.builder; -import java.io.OutputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.io.IOUtils; + +import java.io.IOException; public interface Transferable { int DEFAULT_FILE_MODE = 0100644; + int DEFAULT_DIR_MODE = 040755; /** * Get file mode. Default is 0100644. - * @see Transferable#DEFAULT_FILE_MODE * * @return file mode + * @see Transferable#DEFAULT_FILE_MODE */ default int getFileMode() { return DEFAULT_FILE_MODE; @@ -26,7 +31,26 @@ default int getFileMode() { /** * transfer content of this Transferable to the output stream. Must not close the stream. * - * @param outputStream stream to output + * @param tarArchiveOutputStream stream to output + * @param destination */ - void transferTo(OutputStream outputStream); + default void transferTo(TarArchiveOutputStream tarArchiveOutputStream, final String destination) { + TarArchiveEntry tarEntry = new TarArchiveEntry(destination); + tarEntry.setSize(getSize()); + tarEntry.setMode(getFileMode()); + + try { + tarArchiveOutputStream.putArchiveEntry(tarEntry); + IOUtils.write(getBytes(), tarArchiveOutputStream); + tarArchiveOutputStream.closeArchiveEntry(); + } catch (IOException e) { + throw new RuntimeException("Can't transfer " + getDescription(), e); + } + } + + default byte[] getBytes() { + return new byte[0]; + } + + String getDescription(); } diff --git a/core/src/main/java/org/testcontainers/images/builder/traits/DockerfileTrait.java b/core/src/main/java/org/testcontainers/images/builder/traits/DockerfileTrait.java index a4740220fc8..07ed13ed0fb 100644 --- a/core/src/main/java/org/testcontainers/images/builder/traits/DockerfileTrait.java +++ b/core/src/main/java/org/testcontainers/images/builder/traits/DockerfileTrait.java @@ -1,12 +1,9 @@ package org.testcontainers.images.builder.traits; import lombok.Getter; -import org.apache.commons.io.IOUtils; import org.testcontainers.images.builder.Transferable; import org.testcontainers.images.builder.dockerfile.DockerfileBuilder; -import java.io.IOException; -import java.io.OutputStream; import java.util.function.Consumer; /** @@ -33,12 +30,8 @@ public long getSize() { } @Override - public void transferTo(OutputStream outputStream) { - try { - IOUtils.write(getBytes(), outputStream); - } catch (IOException e) { - throw new RuntimeException("Can't transfer Dockerfile", e); - } + public String getDescription() { + return "Dockerfile: " + builder; } }); } diff --git a/core/src/main/java/org/testcontainers/images/builder/traits/FilesTrait.java b/core/src/main/java/org/testcontainers/images/builder/traits/FilesTrait.java index d837d360a75..882c0e4231b 100644 --- a/core/src/main/java/org/testcontainers/images/builder/traits/FilesTrait.java +++ b/core/src/main/java/org/testcontainers/images/builder/traits/FilesTrait.java @@ -1,11 +1,8 @@ package org.testcontainers.images.builder.traits; -import org.testcontainers.images.builder.Transferable; +import org.testcontainers.utility.MountableFile; import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.file.Files; import java.nio.file.Path; /** @@ -19,31 +16,7 @@ default SELF withFileFromFile(String path, File file) { } default SELF withFileFromPath(String path, Path filePath) { - return ((SELF) this).withFileFromTransferable(path, new Transferable() { - - @Override - public long getSize() { - try { - return Files.size(filePath); - } catch (IOException e) { - throw new RuntimeException("Can't get size from " + filePath, e); - } - } - - @Override - public int getFileMode() { - return DEFAULT_FILE_MODE | (Files.isExecutable(filePath) ? 0755 : 0); - } - - @Override - public void transferTo(OutputStream outputStream) { - try { - Files.copy(filePath, outputStream); - } catch (IOException e) { - throw new RuntimeException("Can't transfer file " + filePath, e); - } - } - - }); + final MountableFile mountableFile = MountableFile.forHostPath(filePath); + return ((SELF) this).withFileFromTransferable(path, mountableFile); } } diff --git a/core/src/main/java/org/testcontainers/images/builder/traits/StringsTrait.java b/core/src/main/java/org/testcontainers/images/builder/traits/StringsTrait.java index 5a3f6bf0f95..51423068ec9 100644 --- a/core/src/main/java/org/testcontainers/images/builder/traits/StringsTrait.java +++ b/core/src/main/java/org/testcontainers/images/builder/traits/StringsTrait.java @@ -1,12 +1,9 @@ package org.testcontainers.images.builder.traits; -import org.apache.commons.io.IOUtils; +import lombok.Getter; import org.apache.commons.lang.StringUtils; import org.testcontainers.images.builder.Transferable; -import java.io.IOException; -import java.io.OutputStream; - /** * BuildContextBuilder's trait for String-based manipulations. * @@ -16,6 +13,7 @@ public interface StringsTrait & BuildContextBuil default SELF withFileFromString(String path, String content) { return ((SELF) this).withFileFromTransferable(path, new Transferable() { + @Getter byte[] bytes = content.getBytes(); @Override @@ -24,14 +22,9 @@ public long getSize() { } @Override - public void transferTo(OutputStream outputStream) { - try { - IOUtils.write(bytes, outputStream); - } catch (IOException e) { - throw new RuntimeException("Can't transfer string " + StringUtils.abbreviate(content, 100), e); - } + public String getDescription() { + return "String: " + StringUtils.abbreviate(content, 100); } - }); } } diff --git a/core/src/main/java/org/testcontainers/utility/MountableFile.java b/core/src/main/java/org/testcontainers/utility/MountableFile.java index 31ee6008751..8fef823274c 100644 --- a/core/src/main/java/org/testcontainers/utility/MountableFile.java +++ b/core/src/main/java/org/testcontainers/utility/MountableFile.java @@ -4,35 +4,34 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.apache.commons.lang.SystemUtils; import org.jetbrains.annotations.NotNull; +import org.testcontainers.images.builder.Transferable; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; +import java.io.*; import java.net.URL; import java.net.URLDecoder; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Enumeration; import java.util.HashSet; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; -import static lombok.AccessLevel.PRIVATE; +import static lombok.AccessLevel.PACKAGE; import static org.testcontainers.utility.PathUtils.recursiveDeleteDir; /** * An abstraction over files and classpath resources aimed at encapsulating all the complexity of generating * a path that the Docker daemon is about to create a volume mount for. */ -@RequiredArgsConstructor(access = PRIVATE) +@RequiredArgsConstructor(access = PACKAGE) @Slf4j -public class MountableFile { +public class MountableFile implements Transferable { private final String path; @@ -60,25 +59,13 @@ public static MountableFile forHostPath(@NotNull final String path) { } /** - * Obtain a path that the Docker daemon should be able to use to volume mount a file/resource - * into a container. If this is a classpath resource residing in a JAR, it will be extracted to - * a temporary location so that the Docker daemon is able to access it. + * Obtains a {@link MountableFile} corresponding to a file on the docker host filesystem. * - * @return a volume-mountable path. + * @param path the path to the resource + * @return a {@link MountableFile} that may be used to obtain a mountable path */ - private String resolvePath() { - String result; - if (path.contains(".jar!")) { - result = extractClassPathResourceToTempLocation(this.path); - } else { - result = unencodeResourceURIToFilePath(path); - } - - if (SystemUtils.IS_OS_WINDOWS) { - result = PathUtils.createMinGWPath(result); - } - - return result; + public static MountableFile forHostPath(final Path path) { + return new MountableFile(path.toAbsolutePath().toString()); } @NotNull @@ -105,18 +92,43 @@ private static URL getClasspathResource(@NotNull final String resourcePath, @Not } } - throw new IllegalArgumentException("Resource with path " + resourcePath + " could not be found on any of these classloaders: " + classLoaders); + throw new IllegalArgumentException("Resource with path " + resourcePath + " could not be found on any of these classloaders: " + classLoadersToSearch); } private static String unencodeResourceURIToFilePath(@NotNull final String resource) { try { // Convert any url-encoded characters (e.g. spaces) back into unencoded form - return new URI(resource).getPath(); - } catch (URISyntaxException e) { + return URLDecoder.decode(resource, Charsets.UTF_8.name()) + .replaceFirst("jar:", "") + .replaceFirst("file:", "") + .replaceAll("!.*", ""); + } catch (UnsupportedEncodingException e) { throw new IllegalStateException(e); } } + /** + * Obtain a path that the Docker daemon should be able to use to volume mount a file/resource + * into a container. If this is a classpath resource residing in a JAR, it will be extracted to + * a temporary location so that the Docker daemon is able to access it. + * + * @return a volume-mountable path. + */ + private String resolvePath() { + String result; + if (path.contains(".jar!")) { + result = extractClassPathResourceToTempLocation(this.path); + } else { + result = unencodeResourceURIToFilePath(path); + } + + if (SystemUtils.IS_OS_WINDOWS) { + result = PathUtils.createMinGWPath(result); + } + + return result; + } + /** * Extract a file or directory tree from a JAR file to a temporary location. * This allows Docker to mount classpath resources as files. @@ -129,13 +141,7 @@ private String extractClassPathResourceToTempLocation(final String hostPath) { //noinspection ResultOfMethodCallIgnored tmpLocation.delete(); - String jarPath = hostPath.replaceFirst("jar:", "").replaceFirst("file:", "").replaceAll("!.*", ""); - String urldecodedJarPath; - try { - urldecodedJarPath = URLDecoder.decode(jarPath, Charsets.UTF_8.name()); - } catch (UnsupportedEncodingException e) { - throw new IllegalArgumentException("Could not URLDecode path with UTF-8 encoding: " + hostPath, e); - } + String urldecodedJarPath = unencodeResourceURIToFilePath(hostPath); String internalPath = hostPath.replaceAll("[^!]*!/", ""); try (JarFile jarFile = new JarFile(urldecodedJarPath)) { @@ -164,9 +170,9 @@ private String extractClassPathResourceToTempLocation(final String hostPath) { @SuppressWarnings("ResultOfMethodCallIgnored") private void copyFromJarToLocation(final JarFile jarFile, - final JarEntry entry, - final String fromRoot, - final File toRoot) throws IOException { + final JarEntry entry, + final String fromRoot, + final File toRoot) throws IOException { String destinationName = entry.getName().replaceFirst(fromRoot, ""); File newFile = new File(toRoot, destinationName); @@ -193,4 +199,84 @@ private void copyFromJarToLocation(final JarFile jarFile, private void deleteOnExit(final Path path) { Runtime.getRuntime().addShutdownHook(new Thread(() -> recursiveDeleteDir(path))); } -} + + /** + * {@inheritDoc} + */ + @Override + public void transferTo(final TarArchiveOutputStream outputStream, String destinationPathInTar) { + recursiveTar(destinationPathInTar, this.getResolvedPath(), this.getResolvedPath(), outputStream); + } + + /* + * Recursively copies a file/directory into a TarArchiveOutputStream + */ + private void recursiveTar(String destination, String sourceRootDir, String sourceCurrentItem, TarArchiveOutputStream tarArchive) { + try { + final File sourceFile = new File(sourceCurrentItem).getCanonicalFile(); // e.g. /foo/bar/baz + final File sourceRootFile = new File(sourceRootDir).getCanonicalFile(); // e.g. /foo + final String relativePathToSourceFile = sourceRootFile.toPath().relativize(sourceFile.toPath()).toFile().toString(); // e.g. /bar/baz + + final TarArchiveEntry tarEntry = new TarArchiveEntry(sourceFile, destination + "/" + relativePathToSourceFile); // entry filename e.g. /xyz/bar/baz + + // TarArchiveEntry automatically sets the mode for file/directory, but we can update to ensure that the mode is set exactly (inc executable bits) + tarEntry.setMode(getUnixFileMode(sourceCurrentItem)); + tarArchive.putArchiveEntry(tarEntry); + + if (sourceFile.isFile()) { + Files.copy(sourceFile.toPath(), tarArchive); + } + // a directory entry merely needs to exist in the TAR file - there is no data stored yet + tarArchive.closeArchiveEntry(); + + final File[] children = sourceFile.listFiles(); + if (children != null) { + // recurse into child files/directories + for (final File child : children) { + recursiveTar(destination, sourceRootDir + File.separator, child.getCanonicalPath(), tarArchive); + } + } + } catch (IOException e) { + log.error("Error when copying TAR file entry: {}", sourceCurrentItem, e); + throw new UncheckedIOException(e); // fail fast + } + } + + @Override + public long getSize() { + + final File file = new File(this.getResolvedPath()); + if (file.isFile()) { + return file.length(); + } else { + return 0; + } + } + + @Override + public String getDescription() { + return this.getResolvedPath(); + } + + @Override + public int getFileMode() { + return getUnixFileMode(this.getResolvedPath()); + } + + private int getUnixFileMode(final String pathAsString) { + final Path path = Paths.get(pathAsString); + try { + return (int) Files.getAttribute(path, "unix:mode"); + } catch (IOException e) { + // fallback for non-posix environments + int mode = DEFAULT_FILE_MODE; + if (Files.isDirectory(path)) { + mode = DEFAULT_DIR_MODE; + } else if (Files.isExecutable(path)) { + mode |= 0111; // equiv to +x for user/group/others + } + + return mode; + } + } +} \ No newline at end of file diff --git a/core/src/test/java/org/testcontainers/junit/DockerfileTest.java b/core/src/test/java/org/testcontainers/junit/DockerfileTest.java index a2b11e5bc9d..7390b21fce2 100644 --- a/core/src/test/java/org/testcontainers/junit/DockerfileTest.java +++ b/core/src/test/java/org/testcontainers/junit/DockerfileTest.java @@ -10,7 +10,6 @@ import org.testcontainers.images.builder.ImageFromDockerfile; import org.testcontainers.images.builder.Transferable; -import java.io.OutputStream; import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; @@ -85,14 +84,21 @@ public long getSize() { } @Override - public int getFileMode() { - return 0123; + public byte[] getBytes() { + return new byte[0]; } @Override - public void transferTo(OutputStream outputStream) { + public String getDescription() { + return "test file"; + } + @Override + public int getFileMode() { + return 0123; } + + }) .withDockerfileFromBuilder(builder -> builder .from("alpine:3.2") diff --git a/core/src/test/java/org/testcontainers/utility/DirectoryTarResourceTest.java b/core/src/test/java/org/testcontainers/utility/DirectoryTarResourceTest.java new file mode 100644 index 00000000000..c4b719ea1ca --- /dev/null +++ b/core/src/test/java/org/testcontainers/utility/DirectoryTarResourceTest.java @@ -0,0 +1,71 @@ +package org.testcontainers.utility; + +import org.junit.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.ToStringConsumer; +import org.testcontainers.containers.output.WaitingConsumer; +import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; +import org.testcontainers.images.builder.ImageFromDockerfile; + +import java.io.File; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue; + +public class DirectoryTarResourceTest { + + @Test + public void simpleRecursiveFileTest() throws TimeoutException { + + WaitingConsumer wait = new WaitingConsumer(); + + final ToStringConsumer toString = new ToStringConsumer(); + + GenericContainer container = new GenericContainer( + new ImageFromDockerfile() + .withDockerfileFromBuilder(builder -> + builder.from("alpine:3.3") + .copy("/tmp/foo", "/foo") + .cmd("cat /foo/src/test/resources/test-recursive-file.txt") + .build() + ).withFileFromFile("/tmp/foo", new File("."))) // '.' is expected to be the project base directory, so all source code/resources should be copied in + .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) + .withLogConsumer(wait.andThen(toString)); + + container.start(); + wait.waitUntilEnd(60, TimeUnit.SECONDS); + + final String results = toString.toUtf8String(); + + assertTrue("The container has a file that was copied in via a recursive copy", results.contains("Used for DirectoryTarResourceTest")); + } + + @Test + public void simpleRecursiveClasspathResourceTest() throws TimeoutException { + // This test combines the copying of classpath resources from JAR files with the recursive TAR approach, to allow JARed classpath resources to be copied in to an image + + WaitingConsumer wait = new WaitingConsumer(); + + final ToStringConsumer toString = new ToStringConsumer(); + + GenericContainer container = new GenericContainer( + new ImageFromDockerfile() + .withDockerfileFromBuilder(builder -> + builder.from("alpine:3.3") + .copy("/tmp/foo", "/foo") + .cmd("ls -lRt /foo") + .build() + ).withFileFromClasspath("/tmp/foo", "/recursive/dir")) // here we use /org/junit as a directory that really should exist on the classpath + .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) + .withLogConsumer(wait.andThen(toString)); + + container.start(); + wait.waitUntilEnd(60, TimeUnit.SECONDS); + + final String results = toString.toUtf8String(); + + // ExternalResource.class is known to exist in a subdirectory of /org/junit so should be successfully copied in + assertTrue("The container has a file that was copied in via a recursive copy from a JAR resource", results.contains("content.txt")); + } +} diff --git a/core/src/test/java/org/testcontainers/utility/MountableFileTest.java b/core/src/test/java/org/testcontainers/utility/MountableFileTest.java index e42d92f07c1..5ac1a1c0ed8 100644 --- a/core/src/test/java/org/testcontainers/utility/MountableFileTest.java +++ b/core/src/test/java/org/testcontainers/utility/MountableFileTest.java @@ -29,7 +29,14 @@ public void forClasspathResourceWithAbsolutePath() throws Exception { @Test public void forClasspathResourceFromJar() throws Exception { - final MountableFile mountableFile = MountableFile.forClasspathResource("docker-java.properties"); + final MountableFile mountableFile = MountableFile.forClasspathResource("META-INF/dummy_unique_name.txt"); + + performChecks(mountableFile); + } + + @Test + public void forClasspathResourceFromJarWithAbsolutePath() throws Exception { + final MountableFile mountableFile = MountableFile.forClasspathResource("/META-INF/dummy_unique_name.txt"); performChecks(mountableFile); } @@ -49,7 +56,8 @@ public void forHostPathWithSpaces() throws Exception { performChecks(mountableFile); - assertTrue("The resolved path contains the original space", mountableFile.getResolvedPath().contains(" "));assertFalse("The resolved path does not contain an escaped space", mountableFile.getResolvedPath().contains("\\ ")); + assertTrue("The resolved path contains the original space", mountableFile.getResolvedPath().contains(" ")); + assertFalse("The resolved path does not contain an escaped space", mountableFile.getResolvedPath().contains("\\ ")); } /* diff --git a/core/src/test/resources/test-recursive-file.txt b/core/src/test/resources/test-recursive-file.txt new file mode 100644 index 00000000000..067480d972b --- /dev/null +++ b/core/src/test/resources/test-recursive-file.txt @@ -0,0 +1 @@ +Used for DirectoryTarResourceTest \ No newline at end of file diff --git a/core/testlib/META-INF/dummy_unique_name.txt b/core/testlib/META-INF/dummy_unique_name.txt new file mode 100644 index 00000000000..48162f49928 --- /dev/null +++ b/core/testlib/META-INF/dummy_unique_name.txt @@ -0,0 +1 @@ +This is a file inside a JAR archive \ No newline at end of file diff --git a/core/testlib/README.md b/core/testlib/README.md new file mode 100644 index 00000000000..437680a18fd --- /dev/null +++ b/core/testlib/README.md @@ -0,0 +1,3 @@ +This directory contains a synthetic JAR (`fakejar.jar`) that is needed for `org.testcontainers.utility.MountableFileTest`. + +The `create_fakejar.sh` script may be used to recreate it. \ No newline at end of file diff --git a/core/testlib/create_fakejar.sh b/core/testlib/create_fakejar.sh new file mode 100755 index 00000000000..e944cec94bc --- /dev/null +++ b/core/testlib/create_fakejar.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +rm fakejar.jar +zip -r fakejar.jar META-INF/ recursive/ +mv fakejar.jar repo/fakejar/fakejar/0/fakejar-0.jar \ No newline at end of file diff --git a/core/testlib/recursive/dir/content.txt b/core/testlib/recursive/dir/content.txt new file mode 100644 index 00000000000..d4e6444e225 --- /dev/null +++ b/core/testlib/recursive/dir/content.txt @@ -0,0 +1 @@ +This is example content to show recursive mounting of files from inside a JAR archive. \ No newline at end of file diff --git a/core/testlib/repo/fakejar/fakejar/0/fakejar-0.jar b/core/testlib/repo/fakejar/fakejar/0/fakejar-0.jar new file mode 100644 index 0000000000000000000000000000000000000000..9cd86a3c13747f306a3a5565d1f7b70f9e1153f2 GIT binary patch literal 949 zcmWIWW@h1H0D(zMyS%^*D8b1f!{F;0;;8HC=cXST!pXqAlxbY@D3!uivN9*P?^ER{Fwh=Of~IKFH-s^htU zHW#HPmlhRgmZd^mZ}0@C>mg=vu0S<|55%p!<6LAcFcMm-~j+K5=#K^o;t|epuod=A@m>1y9ZYn7cw@v&T`1UwWv@3r1ieVo01pl zf88y($!h|O)5FksiKmI@_!qvOkg#(BYpb+nS0;P+XHSnC*JMu?%`N}Q7~svwB*%;^ zswIH#1OWktw~inhDdJfn5swk+AmeaFJH$9(0%2Iv=!|R}jzj>o1C$7`+JP&Ak)6C3 zXa{n11I+_PHy-maBOBQ~MxfIX=0Tz!XeKD0u$qZ0;*rC_6T?iDWPr + + 4.0.0 + fakejar + fakejar + 0 + POM was created from install:install-file + diff --git a/core/testlib/repo/fakejar/fakejar/maven-metadata-local.xml b/core/testlib/repo/fakejar/fakejar/maven-metadata-local.xml new file mode 100644 index 00000000000..cca5c37c2f1 --- /dev/null +++ b/core/testlib/repo/fakejar/fakejar/maven-metadata-local.xml @@ -0,0 +1,12 @@ + + + fakejar + fakejar + + 0 + + 0 + + 20170414082010 + +