diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java index c258eb2b5f..28ca9bc26a 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java @@ -25,6 +25,7 @@ import com.google.common.collect.ImmutableList; import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -33,6 +34,7 @@ import java.util.List; import java.util.Set; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarConstants; /** * Builds a reproducible layer {@link Blob} from files. The reproducibility is implemented by strips @@ -147,12 +149,21 @@ public Blob build() throws IOException { // Adds all the layer entries as tar entries. for (FileEntry layerEntry : layerEntries) { - // Adds the entries to uniqueTarArchiveEntries, which makes sure all entries are unique and - // adds parent directories for each extraction path. - TarArchiveEntry entry = - new TarArchiveEntry( - layerEntry.getSourceFile(), layerEntry.getExtractionPath().toString()); + TarArchiveEntry entry; + if (Files.isSymbolicLink(layerEntry.getSourceFile())) { + entry = + new TarArchiveEntry( + layerEntry.getExtractionPath().toString(), TarConstants.LF_SYMLINK ); + Path targetPath = Files.readSymbolicLink(layerEntry.getSourceFile()); + entry.setLinkName(targetPath.toString()); + } else { + // Adds the entries to uniqueTarArchiveEntries, which makes sure all entries are unique and + // adds parent directories for each extraction path. + entry = + new TarArchiveEntry( + layerEntry.getSourceFile(), layerEntry.getExtractionPath().toString()); + } // Sets the entry's permissions by masking out the permission bits from the entry's mode (the // lowest 9 bits) then using a bitwise OR to set them to the layerEntry's permissions. entry.setMode((entry.getMode() & ~0777) | layerEntry.getPermissions().getPermissionBits()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java index 880e837d26..ef32ffda80 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java @@ -63,7 +63,7 @@ public void writeAsTarArchiveTo(OutputStream out) throws IOException { */ public void addTarArchiveEntry(TarArchiveEntry entry) { archiveMap.put( - entry, entry.isFile() ? Blobs.from(entry.getPath()) : Blobs.from(ignored -> {}, true)); + entry, (entry.isFile() && ! entry.isSymbolicLink()) ? Blobs.from(entry.getPath()) : Blobs.from(ignored -> {}, true)); } /** diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java index d3860184ae..8e9511fb31 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java @@ -38,8 +38,12 @@ import java.nio.file.StandardOpenOption; import java.nio.file.attribute.FileTime; import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.junit.After; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -47,6 +51,32 @@ /** Tests for {@link ReproducibleLayerBuilder}. */ public class ReproducibleLayerBuilderTest { + + + @After + public void cleanUp() throws IOException, URISyntaxException { + removeLinks(getLinks()); + } + + private List getLinks() throws URISyntaxException { + List linksList = new ArrayList<>(); + Path blobA = Paths.get(Resources.getResource("core/blobA").toURI()); + String resourceDir = blobA.getParent().toString(); + Path link1 = Paths.get( resourceDir, "blob-link1"); + linksList.add(link1); + Path link2 = Paths.get(resourceDir + "/layer/a/b/", "blob-link2"); + linksList.add(link2); + return linksList; + } + + private void removeLinks(List linksList) throws IOException { + for (Path path: linksList) { + if (Files.exists(path)) { + Files.delete(path); + } + } + } + /** * Verifies the correctness of the next {@link TarArchiveEntry} in the {@link * TarArchiveInputStream}. @@ -67,6 +97,24 @@ private static void verifyNextTarArchiveEntry( assertThat(extractedBytes).isEqualTo(expectedBytes); } + /** + * Verifies the correctness of the next {@link TarArchiveEntry} in the {@link + * TarArchiveInputStream}. + * + * @param tarArchiveInputStream the {@link TarArchiveInputStream} to read from + * @param expectedExtractionPath the expected extraction path of the next entry + * @param expectedLinkName the expected link name of the next entry + * @throws IOException if an I/O exception occurs + */ + private static void verifyNextTarArchiveEntryIsLink( + TarArchiveInputStream tarArchiveInputStream, String expectedExtractionPath, String expectedLinkName) + throws IOException { + TarArchiveEntry header = tarArchiveInputStream.getNextTarEntry(); + assertThat(header.getName()).isEqualTo(expectedExtractionPath); + assertThat(header.getLinkName()).isEqualTo(expectedLinkName); + assertThat(header.isSymbolicLink()).isTrue(); + } + /** * Verifies that the next {@link TarArchiveEntry} in the {@link TarArchiveInputStream} is a * directory with correct permissions. @@ -94,11 +142,22 @@ private static FileEntry defaultLayerEntry(Path source, AbsoluteUnixPath destina @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + private String getExtractPath(String parent, Path layerDirectory, Path target) { + + return parent + target.getParent().toString().replaceAll(layerDirectory.getParent().toString(), "") + "/" + target.getFileName().toString(); + } + @Test public void testBuild() throws URISyntaxException, IOException { Path layerDirectory = Paths.get(Resources.getResource("core/layer").toURI()); Path blobA = Paths.get(Resources.getResource("core/blobA").toURI()); + List linksList = getLinks(); + removeLinks(linksList); + for (Path path: linksList) { + Files.createSymbolicLink(path, path.getParent().relativize(blobA)); + } + ReproducibleLayerBuilder layerBuilder = new ReproducibleLayerBuilder( ImmutableList.copyOf( @@ -107,6 +166,8 @@ public void testBuild() throws URISyntaxException, IOException { layerDirectory, AbsoluteUnixPath.get("/extract/here/apple/layer")) .addEntry(blobA, AbsoluteUnixPath.get("/extract/here/apple/blobA")) .addEntry(blobA, AbsoluteUnixPath.get("/extract/here/banana/blobA")) + .addEntry(linksList.get(0), AbsoluteUnixPath.get(getExtractPath("/extract/here/apple/", layerDirectory, linksList.get(0) ))) + .addEntry(linksList.get(1), AbsoluteUnixPath.get(getExtractPath("/extract/here/apple/", layerDirectory, linksList.get(1) ))) .build() .getEntries())); @@ -124,6 +185,7 @@ public void testBuild() throws URISyntaxException, IOException { verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/"); verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/"); verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/"); + verifyNextTarArchiveEntryIsLink(tarArchiveInputStream, "extract/here/apple/blob-link1", "blobA"); verifyNextTarArchiveEntry(tarArchiveInputStream, "extract/here/apple/blobA", blobA); verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/layer/"); verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/layer/a/"); @@ -132,6 +194,7 @@ public void testBuild() throws URISyntaxException, IOException { tarArchiveInputStream, "extract/here/apple/layer/a/b/bar", Paths.get(Resources.getResource("core/layer/a/b/bar").toURI())); + verifyNextTarArchiveEntryIsLink(tarArchiveInputStream, "extract/here/apple/layer/a/b/blob-link2", "../../../blobA"); verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/layer/c/"); verifyNextTarArchiveEntry( tarArchiveInputStream,