diff --git a/core/build.gradle b/core/build.gradle index 89028d0f9c3..5727c849fea 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -45,6 +45,8 @@ tasks.japicmp { "org.testcontainers.utility.ResourceReaper#start(com.github.dockerjava.api.DockerClient)", "org.testcontainers.utility.ResourceReaper#registerNetworkForCleanup(java.lang.String)", "org.testcontainers.utility.ResourceReaper#removeNetworks(java.lang.String)", + "org.testcontainers.images.builder.Transferable#of(java.lang.String)", + "org.testcontainers.images.builder.Transferable#updateChecksum(java.util.zip.Checksum)" ] fieldExcludes = [] diff --git a/core/src/main/java/org/testcontainers/containers/Container.java b/core/src/main/java/org/testcontainers/containers/Container.java index d9291fc9fc8..8fd53fc5a0e 100644 --- a/core/src/main/java/org/testcontainers/containers/Container.java +++ b/core/src/main/java/org/testcontainers/containers/Container.java @@ -12,6 +12,7 @@ import org.testcontainers.containers.startupcheck.StartupCheckStrategy; import org.testcontainers.containers.traits.LinkableContainer; import org.testcontainers.containers.wait.strategy.WaitStrategy; +import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.LogUtils; import org.testcontainers.utility.MountableFile; @@ -169,11 +170,23 @@ default SELF withFileSystemBind(String hostPath, String containerPath) { * Set the file to be copied before starting a created container * * @param mountableFile a Mountable file with path of source file / folder on host machine - * @param containerPath a destination path on conatiner to which the files / folders to be copied + * @param containerPath a destination path on container to which the files / folders to be copied * @return this + * + * @deprecated Use {@link #withCopyToContainer(Transferable, String)} instead */ + @Deprecated SELF withCopyFileToContainer(MountableFile mountableFile, String containerPath); + /** + * Set the content to be copied before starting a created container + * + * @param transferable a Transferable + * @param containerPath a destination path on container to which the files / folders to be copied + * @return this + */ + SELF withCopyToContainer(Transferable transferable, String containerPath); + /** * Add an environment variable to be passed to the container. * diff --git a/core/src/main/java/org/testcontainers/containers/GenericContainer.java b/core/src/main/java/org/testcontainers/containers/GenericContainer.java index 70e080711a7..d47bc6cfc5e 100644 --- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java +++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java @@ -23,6 +23,7 @@ import com.google.common.hash.Hashing; import lombok.AccessLevel; import lombok.Data; +import lombok.Getter; import lombok.NonNull; import lombok.Setter; import lombok.SneakyThrows; @@ -49,6 +50,7 @@ import org.testcontainers.containers.wait.strategy.WaitStrategyTarget; import org.testcontainers.images.ImagePullPolicy; import org.testcontainers.images.RemoteDockerImage; +import org.testcontainers.images.builder.Transferable; import org.testcontainers.lifecycle.Startable; import org.testcontainers.lifecycle.Startables; import org.testcontainers.lifecycle.TestDescription; @@ -186,8 +188,15 @@ public class GenericContainer> private Long shmSize; // Maintain order in which entries are added, as earlier target location may be a prefix of a later location. + @Deprecated private Map copyToFileContainerPathMap = new LinkedHashMap<>(); + // Maintain order in which entries are added, as earlier target location may be a prefix of a later location. + @Setter(AccessLevel.NONE) + @Getter(AccessLevel.MODULE) + @VisibleForTesting + private Map copyToTransferableContainerPathMap = new LinkedHashMap<>(); + protected final Set dependencies = new HashSet<>(); /** @@ -415,6 +424,8 @@ private void tryStart(Instant startedAt) { // TODO use single "copy" invocation (and calculate an hash of the resulting tar archive) copyToFileContainerPathMap.forEach(this::copyFileToContainer); + + copyToTransferableContainerPathMap.forEach(this::copyFileToContainer); } connectToPortForwardingNetwork(createCommand.getNetworkMode()); @@ -530,33 +541,18 @@ private void tryStart(Instant startedAt) { @VisibleForTesting Checksum hashCopiedFiles() { Checksum checksum = new Adler32(); - copyToFileContainerPathMap.entrySet().stream().sorted(Entry.comparingByValue()).forEach(entry -> { - byte[] pathBytes = entry.getValue().getBytes(); - // Add path to the hash - checksum.update(pathBytes, 0, pathBytes.length); - - File file = new File(entry.getKey().getResolvedPath()); - checksumFile(file, checksum); - }); + Stream.of(copyToFileContainerPathMap, copyToTransferableContainerPathMap) + .flatMap(it -> it.entrySet().stream()) + .sorted(Entry.comparingByValue()).forEach(entry -> { + byte[] pathBytes = entry.getValue().getBytes(); + // Add path to the hash + checksum.update(pathBytes, 0, pathBytes.length); + + entry.getKey().updateChecksum(checksum); + }); return checksum; } - @VisibleForTesting - @SneakyThrows(IOException.class) - void checksumFile(File file, Checksum checksum) { - Path path = file.toPath(); - checksum.update(MountableFile.getUnixFileMode(path)); - if (file.isDirectory()) { - try (Stream stream = Files.walk(path)) { - stream.filter(it -> it != path).forEach(it -> { - checksumFile(it.toFile(), checksum); - }); - } - } else { - FileUtils.checksum(file, checksum); - } - } - @UnstableAPI @SneakyThrows(JsonProcessingException.class) final String hash(CreateContainerCmd createCommand) { @@ -1296,6 +1292,15 @@ public SELF withCopyFileToContainer(MountableFile mountableFile, String containe return self(); } + /** + * {@inheritDoc} + */ + @Override + public SELF withCopyToContainer(Transferable transferable, String containerPath) { + copyToTransferableContainerPathMap.put(transferable, containerPath); + return self(); + } + /** * Get the IP address that this container may be reached on (may not be the local machine). * 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 3f068f98a18..8ec26c8c3da 100644 --- a/core/src/main/java/org/testcontainers/images/builder/Transferable.java +++ b/core/src/main/java/org/testcontainers/images/builder/Transferable.java @@ -5,12 +5,18 @@ import org.apache.commons.io.IOUtils; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.zip.Checksum; public interface Transferable { int DEFAULT_FILE_MODE = 0100644; int DEFAULT_DIR_MODE = 040755; + static Transferable of(String string) { + return of(string.getBytes(StandardCharsets.UTF_8)); + } + static Transferable of(byte[] bytes) { return of(bytes, DEFAULT_FILE_MODE); } @@ -27,6 +33,11 @@ public byte[] getBytes() { return bytes; } + @Override + public void updateChecksum(Checksum checksum) { + checksum.update(bytes, 0, bytes.length); + } + @Override public int getFileMode() { return fileMode; @@ -78,4 +89,8 @@ default byte[] getBytes() { default String getDescription() { return ""; } + + default void updateChecksum(Checksum checksum) { + throw new UnsupportedOperationException("Provide implementation in subclass"); + } } diff --git a/core/src/main/java/org/testcontainers/utility/MountableFile.java b/core/src/main/java/org/testcontainers/utility/MountableFile.java index 19dca4c134c..81cc37adeab 100644 --- a/core/src/main/java/org/testcontainers/utility/MountableFile.java +++ b/core/src/main/java/org/testcontainers/utility/MountableFile.java @@ -1,12 +1,15 @@ package org.testcontainers.utility; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Charsets; import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; 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.compress.archivers.tar.TarConstants; +import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.SystemUtils; import org.jetbrains.annotations.NotNull; import org.testcontainers.DockerClientFactory; @@ -28,6 +31,8 @@ import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; +import java.util.stream.Stream; +import java.util.zip.Checksum; import static lombok.AccessLevel.PACKAGE; import static org.testcontainers.utility.PathUtils.recursiveDeleteDir; @@ -361,6 +366,27 @@ public String getDescription() { return this.getResolvedPath(); } + @Override + public void updateChecksum(Checksum checksum) { + File file = new File(getResolvedPath()); + checksumFile(file, checksum); + } + + @SneakyThrows(IOException.class) + private void checksumFile(File file, Checksum checksum) { + Path path = file.toPath(); + checksum.update(MountableFile.getUnixFileMode(path)); + if (file.isDirectory()) { + try (Stream stream = Files.walk(path)) { + stream.filter(it -> it != path).forEach(it -> { + checksumFile(it.toFile(), checksum); + }); + } + } else { + FileUtils.checksum(file, checksum); + } + } + @Override public int getFileMode() { return getUnixFileMode(this.getResolvedPath()); diff --git a/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java b/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java index 93a9cd96479..0c30d522cd9 100644 --- a/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java +++ b/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java @@ -20,7 +20,11 @@ import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; import org.testcontainers.containers.startupcheck.StartupCheckStrategy; import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy; +import org.testcontainers.images.builder.Transferable; + +import java.nio.charset.StandardCharsets; import org.testcontainers.images.builder.ImageFromDockerfile; +import org.testcontainers.utility.MountableFile; import java.util.Arrays; import java.util.List; @@ -75,6 +79,38 @@ public void shouldReportErrorAfterWait() { } } + @Test + public void shouldCopyTransferableAsFile() { + try ( + GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) + .withStartupCheckStrategy(new NoopStartupCheckStrategy()) + .withCopyToContainer(Transferable.of("test"), "/tmp/test") + .waitingFor(new WaitForExitedState(state -> state.getExitCodeLong() > 0)) + .withCommand("sh", "-c", "grep -q test /tmp/test && exit 100") + ) { + assertThatThrownBy(container::start) + .hasStackTraceContaining("Container exited with code 100"); + } + } + + @Test + public void shouldCopyTransferableAfterMountableFile() { + try ( + GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) + .withStartupCheckStrategy(new NoopStartupCheckStrategy()) + .withCopyFileToContainer( + MountableFile.forClasspathResource("test_copy_to_container.txt"), + "/tmp/test" + ) + .withCopyToContainer(Transferable.of("test"), "/tmp/test") + .waitingFor(new WaitForExitedState(state -> state.getExitCodeLong() > 0)) + .withCommand("sh", "-c", "grep -q test /tmp/test && exit 100") + ) { + assertThatThrownBy(container::start) + .hasStackTraceContaining("Container exited with code 100"); + } + } + @Test public void shouldOnlyPublishExposedPorts() { ImageFromDockerfile image = new ImageFromDockerfile("publish-multiple") diff --git a/core/src/test/java/org/testcontainers/containers/ReusabilityUnitTests.java b/core/src/test/java/org/testcontainers/containers/ReusabilityUnitTests.java index 0540fcdba9d..9aad97d7e7b 100644 --- a/core/src/test/java/org/testcontainers/containers/ReusabilityUnitTests.java +++ b/core/src/test/java/org/testcontainers/containers/ReusabilityUnitTests.java @@ -10,7 +10,6 @@ import com.github.dockerjava.api.command.ListContainersCmd; import com.github.dockerjava.api.command.StartContainerCmd; import com.github.dockerjava.api.model.Container; -import com.github.dockerjava.api.model.NetworkSettings; import com.github.dockerjava.core.command.CreateContainerCmdImpl; import com.github.dockerjava.core.command.InspectContainerCmdImpl; import com.github.dockerjava.core.command.ListContainersCmdImpl; @@ -24,13 +23,13 @@ import org.junit.runners.BlockJUnit4ClassRunner; import org.junit.runners.Parameterized; import org.mockito.Answers; -import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.stubbing.Answer; import org.rnorth.visibleassertions.VisibleAssertions; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.startupcheck.StartupCheckStrategy; import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy; +import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.MockTestcontainersConfigurationRule; import org.testcontainers.utility.MountableFile; import org.testcontainers.utility.TestcontainersConfiguration; @@ -39,12 +38,14 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -297,11 +298,65 @@ public void shouldHashCopiedFiles() { } } - @RunWith(BlockJUnit4ClassRunner.class) + @RunWith(Parameterized.class) @FieldDefaults(makeFinal = true) public static class CopyFilesHashTest { + private final TestStrategy strategy; + + interface TestStrategy { + void withCopyFileToContainer(MountableFile mountableFile, String path); + + void clear(); + } + + private static class MountableFileTestStrategy implements TestStrategy { + private final GenericContainer container; + + private MountableFileTestStrategy(GenericContainer container) { + this.container = container; + } + + @Override + public void withCopyFileToContainer(MountableFile mountableFile, String path) { + container.withCopyFileToContainer(mountableFile, path); + } + + @Override + public void clear() { + container.getCopyToFileContainerPathMap().clear(); + } + } + + private static class TransferableTestStrategy implements TestStrategy { + private final GenericContainer container; + + private TransferableTestStrategy(GenericContainer container) { + this.container = container; + } + + @Override + public void withCopyFileToContainer(MountableFile mountableFile, String path) { + container.withCopyToContainer(mountableFile, path); + } + + @Override + public void clear() { + container.getCopyToTransferableContainerPathMap().clear(); + } + } + + @Parameterized.Parameters + public static List, TestStrategy>> strategies() { + return Arrays.asList(MountableFileTestStrategy::new, TransferableTestStrategy::new); + } + + GenericContainer container = new GenericContainer<>(TINY_IMAGE); + public CopyFilesHashTest(Function, TestStrategy> strategyFactory) { + this.strategy = strategyFactory.apply(container); + } + @Test public void empty() { assertThat(container.hashCopiedFiles()).isNotNull(); @@ -311,7 +366,7 @@ public void empty() { public void oneFile() { long emptyHash = container.hashCopiedFiles().getValue(); - container.withCopyFileToContainer( + strategy.withCopyFileToContainer( MountableFile.forClasspathResource("test_copy_to_container.txt"), "/foo/bar" ); @@ -322,13 +377,13 @@ public void oneFile() { @Test public void differentPath() { MountableFile mountableFile = MountableFile.forClasspathResource("test_copy_to_container.txt"); - container.withCopyFileToContainer(mountableFile, "/foo/bar"); + strategy.withCopyFileToContainer(mountableFile, "/foo/bar"); long hash1 = container.hashCopiedFiles().getValue(); - container.getCopyToFileContainerPathMap().clear(); + strategy.clear(); - container.withCopyFileToContainer(mountableFile, "/foo/baz"); + strategy.withCopyFileToContainer(mountableFile, "/foo/baz"); assertThat(container.hashCopiedFiles().getValue()).isNotEqualTo(hash1); } @@ -337,7 +392,7 @@ public void differentPath() { public void detectsChangesInFile() throws Exception { Path path = File.createTempFile("reusable_test", ".txt").toPath(); MountableFile mountableFile = MountableFile.forHostPath(path); - container.withCopyFileToContainer(mountableFile, "/foo/bar"); + strategy.withCopyFileToContainer(mountableFile, "/foo/bar"); long hash1 = container.hashCopiedFiles().getValue(); @@ -348,13 +403,13 @@ public void detectsChangesInFile() throws Exception { @Test public void multipleFiles() { - container.withCopyFileToContainer( + strategy.withCopyFileToContainer( MountableFile.forClasspathResource("test_copy_to_container.txt"), "/foo/bar" ); long hash1 = container.hashCopiedFiles().getValue(); - container.withCopyFileToContainer( + strategy.withCopyFileToContainer( MountableFile.forClasspathResource("mappable-resource/test-resource.txt"), "/foo/baz" ); @@ -368,7 +423,7 @@ public void folder() throws Exception { Path tempDirectory = Files.createTempDirectory("reusable_test"); MountableFile mountableFile = MountableFile.forHostPath(tempDirectory); - container.withCopyFileToContainer(mountableFile, "/foo/bar/"); + strategy.withCopyFileToContainer(mountableFile, "/foo/bar/"); assertThat(container.hashCopiedFiles().getValue()).isNotEqualTo(emptyHash); } @@ -378,7 +433,7 @@ public void changesInFolder() throws Exception { Path tempDirectory = Files.createTempDirectory("reusable_test"); MountableFile mountableFile = MountableFile.forHostPath(tempDirectory); assertThat(new File(mountableFile.getResolvedPath())).isDirectory(); - container.withCopyFileToContainer(mountableFile, "/foo/bar/"); + strategy.withCopyFileToContainer(mountableFile, "/foo/bar/"); long hash1 = container.hashCopiedFiles().getValue(); @@ -397,11 +452,11 @@ public void folderAndFile() throws Exception { Path tempDirectory = Files.createTempDirectory("reusable_test"); MountableFile mountableFile = MountableFile.forHostPath(tempDirectory); assertThat(new File(mountableFile.getResolvedPath())).isDirectory(); - container.withCopyFileToContainer(mountableFile, "/foo/bar/"); + strategy.withCopyFileToContainer(mountableFile, "/foo/bar/"); long hash1 = container.hashCopiedFiles().getValue(); - container.withCopyFileToContainer( + strategy.withCopyFileToContainer( MountableFile.forClasspathResource("test_copy_to_container.txt"), "/foo/baz" ); @@ -414,7 +469,7 @@ public void filePermissions() throws Exception { Path path = File.createTempFile("reusable_test", ".txt").toPath(); path.toFile().setExecutable(false); MountableFile mountableFile = MountableFile.forHostPath(path); - container.withCopyFileToContainer(mountableFile, "/foo/bar"); + strategy.withCopyFileToContainer(mountableFile, "/foo/bar"); long hash1 = container.hashCopiedFiles().getValue(); @@ -432,7 +487,7 @@ public void folderPermissions() throws Exception { Path subDir = Files.createDirectory(tempDirectory.resolve("sub")); subDir.toFile().setWritable(false); assumeThat(subDir.toFile().canWrite()).isFalse(); - container.withCopyFileToContainer(mountableFile, "/foo/bar/"); + strategy.withCopyFileToContainer(mountableFile, "/foo/bar/"); long hash1 = container.hashCopiedFiles().getValue();