Skip to content

Commit

Permalink
Allow to create a file in container from a Transferable (#3814) (#3815)
Browse files Browse the repository at this point in the history
Co-authored-by: Alex Stockinger <alex@atomicjar.com>
Co-authored-by: Sergei Egorov <bsideup@gmail.com>
Co-authored-by: Kevin Wittek <kevin@wittek.dev>
  • Loading branch information
4 people committed Apr 8, 2022
1 parent c7449ed commit d93dcea
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 41 deletions.
2 changes: 2 additions & 0 deletions core/build.gradle
Expand Up @@ -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 = []
Expand Down
15 changes: 14 additions & 1 deletion core/src/main/java/org/testcontainers/containers/Container.java
Expand Up @@ -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;

Expand Down Expand Up @@ -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.
*
Expand Down
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -186,8 +188,15 @@ public class GenericContainer<SELF extends GenericContainer<SELF>>
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<MountableFile, String> 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<Transferable, String> copyToTransferableContainerPathMap = new LinkedHashMap<>();

protected final Set<Startable> dependencies = new HashSet<>();

/**
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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<Path> 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) {
Expand Down Expand Up @@ -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).
*
Expand Down
Expand Up @@ -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);
}
Expand All @@ -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;
Expand Down Expand Up @@ -78,4 +89,8 @@ default byte[] getBytes() {
default String getDescription() {
return "";
}

default void updateChecksum(Checksum checksum) {
throw new UnsupportedOperationException("Provide implementation in subclass");
}
}
26 changes: 26 additions & 0 deletions 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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<Path> 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());
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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")
Expand Down

0 comments on commit d93dcea

Please sign in to comment.