From b4aae225d4076996aee334d1f6dedfd23cdec499 Mon Sep 17 00:00:00 2001 From: Mike Pedersen Date: Wed, 25 May 2022 15:42:05 +0200 Subject: [PATCH] Add support for setting owner of MountableFile, default to root --- .../images/builder/Transferable.java | 34 ++++++++++++++ .../testcontainers/utility/MountableFile.java | 46 ++++++++++++++++++- .../utility/MountableFileTest.java | 38 +++++++++++++++ 3 files changed, 116 insertions(+), 2 deletions(-) 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 e98dbc2ad11..35d336ea7ca 100644 --- a/core/src/main/java/org/testcontainers/images/builder/Transferable.java +++ b/core/src/main/java/org/testcontainers/images/builder/Transferable.java @@ -21,6 +21,10 @@ static Transferable of(byte[] bytes) { } static Transferable of(byte[] bytes, int fileMode) { + return of(bytes, fileMode, 0, 0); + } + + static Transferable of(byte[] bytes, int fileMode, int userId, int groupId) { return new Transferable() { @Override public long getSize() { @@ -41,6 +45,16 @@ public void updateChecksum(Checksum checksum) { public int getFileMode() { return fileMode; } + + @Override + public long getUserId() { + return userId; + } + + @Override + public long getGroupId() { + return groupId; + } }; } @@ -61,6 +75,24 @@ default int getFileMode() { */ long getSize(); + /** + * User ID owning the file + * + * @return ID of user owner + */ + default long getUserId() { + return 0; + } + + /** + * Group ID owning the file + * + * @return ID of group owner + */ + default long getGroupId() { + return 0; + } + /** * transfer content of this Transferable to the output stream. Must not close the stream. * @@ -71,6 +103,8 @@ default void transferTo(TarArchiveOutputStream tarArchiveOutputStream, final Str TarArchiveEntry tarEntry = new TarArchiveEntry(destination); tarEntry.setSize(getSize()); tarEntry.setMode(getFileMode()); + tarEntry.setUserId(getUserId()); + tarEntry.setGroupId(getGroupId()); try { tarArchiveOutputStream.putArchiveEntry(tarEntry); diff --git a/core/src/main/java/org/testcontainers/utility/MountableFile.java b/core/src/main/java/org/testcontainers/utility/MountableFile.java index a8a9f465a19..33ae11e066c 100644 --- a/core/src/main/java/org/testcontainers/utility/MountableFile.java +++ b/core/src/main/java/org/testcontainers/utility/MountableFile.java @@ -54,6 +54,10 @@ public class MountableFile implements Transferable { private final Integer forcedFileMode; + private final long userId; + + private final long groupId; + @Getter(lazy = true) private final String resolvedPath = resolvePath(); @@ -100,7 +104,20 @@ public static MountableFile forHostPath(final Path path) { * @return a {@link MountableFile} that may be used to obtain a mountable path */ public static MountableFile forClasspathResource(@NotNull final String resourceName, Integer mode) { - return new MountableFile(getClasspathResource(resourceName, new HashSet<>()).toString(), mode); + return forClasspathResource(getClasspathResource(resourceName, new HashSet<>()).toString(), mode, 0, 0); + } + + /** + * 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) + * @param userId the id of the user owning the file + * @param groupId the id of the group owning the file + * @return a {@link MountableFile} that may be used to obtain a mountable path + */ + public static MountableFile forClasspathResource(@NotNull final String resourceName, Integer mode, int userId, int groupId) { + return new MountableFile(getClasspathResource(resourceName, new HashSet<>()).toString(), mode, userId, groupId); } /** @@ -122,7 +139,20 @@ public static MountableFile forHostPath(@NotNull final String path, Integer mode * @return a {@link MountableFile} that may be used to obtain a mountable path */ public static MountableFile forHostPath(final Path path, Integer mode) { - return new MountableFile(path.toAbsolutePath().toString(), mode); + return new MountableFile(path.toAbsolutePath().toString(), mode, 0, 0); + } + + /** + * 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) + * @param userId the id of the user owning the file + * @param groupId the id of the group owning the file + * @return a {@link MountableFile} that may be used to obtain a mountable path + */ + public static MountableFile forHostPath(final Path path, Integer mode, int userId, int groupId) { + return new MountableFile(path.toAbsolutePath().toString(), mode, userId, groupId); } @NotNull @@ -356,6 +386,8 @@ private void recursiveTar( // 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(itemPath)); + tarEntry.setUserId(userId); + tarEntry.setGroupId(groupId); tarArchive.putArchiveEntry(tarEntry); if (sourceFile.isFile()) { @@ -392,6 +424,16 @@ public long getSize() { } } + @Override + public long getUserId() { + return userId; + } + + @Override + public long getGroupId() { + return groupId; + } + @Override public String getDescription() { return this.getResolvedPath(); diff --git a/core/src/test/java/org/testcontainers/utility/MountableFileTest.java b/core/src/test/java/org/testcontainers/utility/MountableFileTest.java index d3ff085c5de..36e3db77857 100644 --- a/core/src/test/java/org/testcontainers/utility/MountableFileTest.java +++ b/core/src/test/java/org/testcontainers/utility/MountableFileTest.java @@ -2,6 +2,7 @@ import lombok.Cleanup; import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.jetbrains.annotations.NotNull; @@ -133,6 +134,43 @@ public void noTrailingSlashesInTarEntryNames() throws Exception { } } + @Test + public void rootIsDefaultOwner() throws Exception { + final MountableFile mountableFile = MountableFile.forClasspathResource("mappable-resource/test-resource.txt"); + + @Cleanup + final TarArchiveInputStream tais = intoTarArchive(taos -> { + mountableFile.transferTo(taos, "path.txt"); + }); + + TarArchiveEntry entry; + while ((entry = tais.getNextTarEntry()) != null) { + assertEquals("User ID should be 0", 0, entry.getLongUserId()); + assertEquals("Group ID should be 0", 0, entry.getLongGroupId()); + } + } + + @Test + public void canSetOtherOwner() throws Exception { + final MountableFile mountableFile = MountableFile.forClasspathResource( + "mappable-resource/test-resource.txt", + null, + 1, + 2 + ); + + @Cleanup + final TarArchiveInputStream tais = intoTarArchive(taos -> { + mountableFile.transferTo(taos, "path.txt"); + }); + + TarArchiveEntry entry; + while ((entry = tais.getNextTarEntry()) != null) { + assertEquals("User ID should be 1", 1L, entry.getLongUserId()); + assertEquals("Group ID should be 2", 2L, entry.getLongGroupId()); + } + } + private TarArchiveInputStream intoTarArchive(Consumer consumer) throws IOException { @Cleanup final ByteArrayOutputStream baos = new ByteArrayOutputStream();