package org.testcontainers.images.builder;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.BuildImageCmd;
import com.github.dockerjava.api.exception.DockerClientException;
import com.github.dockerjava.api.model.BuildResponseItem;
import com.github.dockerjava.core.command.BuildImageResultCallback;
import com.google.common.collect.Sets;
import lombok.Cleanup;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.profiler.Profiler;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.images.builder.traits.*;
import org.testcontainers.utility.Base58;
import org.testcontainers.utility.DockerLoggerFactory;
import org.testcontainers.utility.LazyFuture;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.zip.GZIPOutputStream;
@Slf4j
@Getter
public class ImageFromDockerfile extends LazyFuture<String> implements
BuildContextBuilderTrait<ImageFromDockerfile>,
ClasspathTrait<ImageFromDockerfile>,
FilesTrait<ImageFromDockerfile>,
StringsTrait<ImageFromDockerfile>,
DockerfileTrait<ImageFromDockerfile> {
private static final Set<String> imagesToDelete = Sets.newConcurrentHashSet();
static {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
DockerClient dockerClientForCleaning = DockerClientFactory.instance().client();
try {
for (String dockerImageName : imagesToDelete) {
log.info("Removing image tagged {}", dockerImageName);
try {
dockerClientForCleaning.removeImageCmd(dockerImageName).withForce(true).exec();
} catch (Throwable e) {
log.warn("Unable to delete image " + dockerImageName, e);
}
}
} catch (DockerClientException e) {
throw new RuntimeException(e);
}
}));
}
private final String dockerImageName;
private boolean deleteOnExit = true;
private final Map<String, Transferable> transferables = new HashMap<>();
public ImageFromDockerfile() {
this("testcontainers/" + Base58.randomString(16).toLowerCase());
}
public ImageFromDockerfile(String dockerImageName) {
this(dockerImageName, true);
}
public ImageFromDockerfile(String dockerImageName, boolean deleteOnExit) {
this.dockerImageName = dockerImageName;
this.deleteOnExit = deleteOnExit;
}
@Override
public ImageFromDockerfile withFileFromTransferable(String path, Transferable transferable) {
Transferable oldValue = transferables.put(path, transferable);
if (oldValue != null) {
log.warn("overriding previous mapping for '{}'", path);
}
return this;
}
@Override
protected final String resolve() {
Logger logger = DockerLoggerFactory.getLogger(dockerImageName);
Profiler profiler = new Profiler("Rule creation - build image");
profiler.setLogger(logger);
DockerClient dockerClient = DockerClientFactory.instance().client();
try {
if (deleteOnExit) {
imagesToDelete.add(dockerImageName);
}
BuildImageResultCallback resultCallback = new BuildImageResultCallback() {
@Override
public void onNext(BuildResponseItem item) {
super.onNext(item);
if (item.isErrorIndicated()) {
logger.error(item.getErrorDetail().getMessage());
} else {
logger.debug(StringUtils.chomp(item.getStream(), "\n"));
}
}
};
// We have to use pipes to avoid high memory consumption since users might want to build really big images
@Cleanup PipedInputStream in = new PipedInputStream();
@Cleanup PipedOutputStream out = new PipedOutputStream(in);
profiler.start("Configure image");
BuildImageCmd buildImageCmd = dockerClient.buildImageCmd(in);
configure(buildImageCmd);
profiler.start("Build image");
BuildImageResultCallback exec = buildImageCmd.exec(resultCallback);
// To build an image, we have to send the context to Docker in TAR archive format
profiler.start("Send context as TAR");
try (TarArchiveOutputStream tarArchive = new TarArchiveOutputStream(new GZIPOutputStream(out))) {
tarArchive.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
for (Map.Entry<String, Transferable> entry : transferables.entrySet()) {
Transferable transferable = entry.getValue();
final String destination = entry.getKey();
transferable.transferTo(tarArchive, destination);
}
tarArchive.finish();
}
profiler.start("Wait for an image id");
exec.awaitImageId();
return dockerImageName;
} catch(IOException e) {
throw new RuntimeException("Can't close DockerClient", e);
} finally {
profiler.stop().log();
}
}
protected void configure(BuildImageCmd buildImageCmd) {
buildImageCmd.withTag(this.getDockerImageName());
}
}