diff --git a/CHANGELOG.md b/CHANGELOG.md index e1f2ae3348e..79bf3eef296 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. ### Changed - Make Network instances reusable (i.e. work with `@ClassRule`) ([\#469](https://github.com/testcontainers/testcontainers-java/issues/469)) +- Added support for explicitly setting file mode when copying file into container (#446) ## [1.4.3] - 2017-10-14 ### Fixed 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 882c0e4231b..26ef980c25b 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 @@ -11,12 +11,46 @@ */ public interface FilesTrait & BuildContextBuilderTrait> { + /** + * Adds file to tarball copied into container. + * @param path in tarball + * @param file in host filesystem + * @return self + */ default SELF withFileFromFile(String path, File file) { - return withFileFromPath(path, file.toPath()); + return withFileFromPath(path, file.toPath(), 0); } + /** + * Adds file to tarball copied into container. + * @param path in tarball + * @param filePath in host filesystem + * @return self + */ default SELF withFileFromPath(String path, Path filePath) { - final MountableFile mountableFile = MountableFile.forHostPath(filePath); + return withFileFromPath(path, filePath, 0); + } + + /** + * Adds file with given mode to tarball copied into container. + * @param path in tarball + * @param file in host filesystem + * @param mode octal value of posix file mode (000..777) + * @return self + */ + default SELF withFileFromFile(String path, File file, int mode) { + return withFileFromPath(path, file.toPath(), mode); + } + + /** + * Adds file with given mode to tarball copied into container. + * @param path in tarball + * @param filePath in host filesystem + * @param mode octal value of posix file mode (000..777) + * @return self + */ + default SELF withFileFromPath(String path, Path filePath, int mode) { + final MountableFile mountableFile = MountableFile.forHostPath(filePath, mode); return ((SELF) this).withFileFromTransferable(path, mountableFile); } } diff --git a/core/src/main/java/org/testcontainers/utility/MountableFile.java b/core/src/main/java/org/testcontainers/utility/MountableFile.java index 06d08c2996d..9fb4e815a83 100644 --- a/core/src/main/java/org/testcontainers/utility/MountableFile.java +++ b/core/src/main/java/org/testcontainers/utility/MountableFile.java @@ -10,7 +10,11 @@ import org.jetbrains.annotations.NotNull; import org.testcontainers.images.builder.Transferable; -import java.io.*; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLDecoder; import java.nio.file.Files; @@ -33,7 +37,11 @@ @Slf4j public class MountableFile implements Transferable { + private static final int BASE_FILE_MODE = 0100000; + private static final int BASE_DIR_MODE = 0040000; + private final String path; + private final int forcedFileMode; @Getter(lazy = true) private final String resolvedPath = resolvePath(); @@ -50,7 +58,7 @@ public class MountableFile implements Transferable { * @return a {@link MountableFile} that may be used to obtain a mountable path */ public static MountableFile forClasspathResource(@NotNull final String resourceName) { - return new MountableFile(getClasspathResource(resourceName, new HashSet<>()).toString()); + return forClasspathResource(resourceName, -1); } /** @@ -60,7 +68,7 @@ public static MountableFile forClasspathResource(@NotNull final String resourceN * @return a {@link MountableFile} that may be used to obtain a mountable path */ public static MountableFile forHostPath(@NotNull final String path) { - return new MountableFile(new File(path).toURI().toString()); + return forHostPath(path, -1); } /** @@ -70,9 +78,43 @@ public static MountableFile forHostPath(@NotNull final String path) { * @return a {@link MountableFile} that may be used to obtain a mountable path */ public static MountableFile forHostPath(final Path path) { - return new MountableFile(path.toAbsolutePath().toString()); + return forHostPath(path, -1); + } + + /** + * Obtains a {@link MountableFile} corresponding to a resource on the classpath (including resources in JAR files) + * + * @param resourceName the classpath path to the resource + * @param mode octal value of posix file mode (000..777) + * @return a {@link MountableFile} that may be used to obtain a mountable path + */ + public static MountableFile forClasspathResource(@NotNull final String resourceName, int mode) { + return new MountableFile(getClasspathResource(resourceName, new HashSet<>()).toString(), mode); + } + + /** + * Obtains a {@link MountableFile} corresponding to a file on the docker host filesystem. + * + * @param path the path to the resource + * @param mode octal value of posix file mode (000..777) + * @return a {@link MountableFile} that may be used to obtain a mountable path + */ + public static MountableFile forHostPath(@NotNull final String path, int mode) { + return new MountableFile(new File(path).toURI().toString(), mode); } + /** + * Obtains a {@link MountableFile} corresponding to a file on the docker host filesystem. + * + * @param path the path to the resource + * @param mode octal value of posix file mode (000..777) + * @return a {@link MountableFile} that may be used to obtain a mountable path + */ + public static MountableFile forHostPath(final Path path, int mode) { + return new MountableFile(path.toAbsolutePath().toString(), mode); + } + + @NotNull private static URL getClasspathResource(@NotNull final String resourcePath, @NotNull final Set classLoaders) { @@ -291,6 +333,10 @@ public int getFileMode() { private int getUnixFileMode(final String pathAsString) { final Path path = Paths.get(pathAsString); + if (this.forcedFileMode > -1) { + return this.getModeValue(path); + } + try { return (int) Files.getAttribute(path, "unix:mode"); } catch (IOException | UnsupportedOperationException e) { @@ -305,4 +351,9 @@ private int getUnixFileMode(final String pathAsString) { return mode; } } + + private int getModeValue(final Path path) { + int result = Files.isDirectory(path) ? BASE_DIR_MODE : BASE_FILE_MODE; + return result | this.forcedFileMode; + } } \ No newline at end of file diff --git a/core/src/test/java/org/testcontainers/utility/DirectoryTarResourceTest.java b/core/src/test/java/org/testcontainers/utility/DirectoryTarResourceTest.java index c4b719ea1ca..70c366e3c80 100644 --- a/core/src/test/java/org/testcontainers/utility/DirectoryTarResourceTest.java +++ b/core/src/test/java/org/testcontainers/utility/DirectoryTarResourceTest.java @@ -1,5 +1,8 @@ package org.testcontainers.utility; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.core.IsCollectionContaining; import org.junit.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.output.ToStringConsumer; @@ -8,9 +11,13 @@ import org.testcontainers.images.builder.ImageFromDockerfile; import java.io.File; +import java.util.Arrays; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.containsString; +import static org.rnorth.visibleassertions.VisibleAssertions.assertThat; import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue; public class DirectoryTarResourceTest { @@ -41,6 +48,35 @@ public void simpleRecursiveFileTest() throws TimeoutException { assertTrue("The container has a file that was copied in via a recursive copy", results.contains("Used for DirectoryTarResourceTest")); } + @Test + public void simpleRecursiveFileWithPermissionTest() 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("ls", "-al", "/") + .build() + ).withFileFromFile("/tmp/foo", new File("/mappable-resource/test-resource.txt"), + 0754)) + .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) + .withLogConsumer(wait.andThen(toString)); + + container.start(); + wait.waitUntilEnd(60, TimeUnit.SECONDS); + + String listing = toString.toUtf8String(); + + assertThat("Listing shows that file is copied with mode requested.", + Arrays.asList(listing.split("\\n")), + exactlyNItems(1, allOf(containsString("-rwxr-xr--"), containsString("foo")))); + } + @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 @@ -68,4 +104,31 @@ public void simpleRecursiveClasspathResourceTest() throws TimeoutException { // 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")); } + + public static Matcher> exactlyNItems(final int n, Matcher elementMatcher) { + return new IsCollectionContaining(elementMatcher) { + @Override + protected boolean matchesSafely(Iterable collection, Description mismatchDescription) { + int count = 0; + boolean isPastFirst = false; + + for (Object item : collection) { + + if (elementMatcher.matches(item)) { + count++; + } + if (isPastFirst) { + mismatchDescription.appendText(", "); + } + elementMatcher.describeMismatch(item, mismatchDescription); + isPastFirst = true; + } + + if (count != n) { + mismatchDescription.appendText(". Expected exactly " + n + " but got " + count); + } + return count == n; + } + }; + } } diff --git a/core/src/test/java/org/testcontainers/utility/MountableFileTest.java b/core/src/test/java/org/testcontainers/utility/MountableFileTest.java index 8e4a40fc5cc..c278953c112 100644 --- a/core/src/test/java/org/testcontainers/utility/MountableFileTest.java +++ b/core/src/test/java/org/testcontainers/utility/MountableFileTest.java @@ -8,11 +8,16 @@ import java.nio.file.Files; import java.nio.file.Path; +import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals; import static org.rnorth.visibleassertions.VisibleAssertions.assertFalse; import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue; public class MountableFileTest { + private static final int TEST_FILE_MODE = 0532; + private static final int BASE_FILE_MODE = 0100000; + private static final int BASE_DIR_MODE = 0040000; + @Test public void forClasspathResource() throws Exception { final MountableFile mountableFile = MountableFile.forClasspathResource("mappable-resource/test-resource.txt"); @@ -60,6 +65,31 @@ public void forHostPathWithSpaces() throws Exception { assertFalse("The resolved path does not contain an escaped space", mountableFile.getResolvedPath().contains("\\ ")); } + @Test + public void forClasspathResourceWithPermission() throws Exception { + final MountableFile mountableFile = MountableFile.forClasspathResource("mappable-resource/test-resource.txt", + TEST_FILE_MODE); + + performChecks(mountableFile); + assertEquals("Valid file mode.", BASE_FILE_MODE | TEST_FILE_MODE, mountableFile.getFileMode()); + } + + @Test + public void forHostFilePathWithPermission() throws Exception { + final Path file = createTempFile("somepath"); + final MountableFile mountableFile = MountableFile.forHostPath(file.toString(), TEST_FILE_MODE); + performChecks(mountableFile); + assertEquals("Valid file mode.", BASE_FILE_MODE | TEST_FILE_MODE, mountableFile.getFileMode()); + } + + @Test + public void forHostDirPathWithPermission() throws Exception { + final Path dir = createTempDir(); + final MountableFile mountableFile = MountableFile.forHostPath(dir.toString(), TEST_FILE_MODE); + performChecks(mountableFile); + assertEquals("Valid dir mode.", BASE_DIR_MODE | TEST_FILE_MODE, mountableFile.getFileMode()); + } + @SuppressWarnings("ResultOfMethodCallIgnored") @NotNull private Path createTempFile(final String name) throws IOException { @@ -72,6 +102,11 @@ private Path createTempFile(final String name) throws IOException { return file; } + @NotNull + private Path createTempDir() throws IOException { + return Files.createTempDirectory("testcontainers"); + } + private void performChecks(final MountableFile mountableFile) { final String mountablePath = mountableFile.getFilesystemPath(); assertTrue("The filesystem path '" + mountablePath + "' can be found", new File(mountablePath).exists());