package org.testcontainers.images;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.ListImagesCmd;
import com.github.dockerjava.api.exception.DockerClientException;
import com.github.dockerjava.api.model.Image;
import com.github.dockerjava.core.command.PullImageResultCallback;
import lombok.NonNull;
import org.slf4j.Logger;
import org.slf4j.profiler.Profiler;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.ContainerFetchException;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.DockerLoggerFactory;
import org.testcontainers.utility.LazyFuture;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class RemoteDockerImage extends LazyFuture<String> {
public static final Set<String> AVAILABLE_IMAGE_NAME_CACHE = new HashSet<>();
private final String dockerImageName;
public RemoteDockerImage(String dockerImageName) {
DockerImageName.validate(dockerImageName);
this.dockerImageName = dockerImageName;
}
public RemoteDockerImage(@NonNull String repository, @NonNull String tag) {
this.dockerImageName = repository + ":" + tag;
}
@Override
protected final String resolve() {
Profiler profiler = new Profiler("Rule creation - prefetch image");
Logger logger = DockerLoggerFactory.getLogger(dockerImageName);
profiler.setLogger(logger);
Profiler nested = profiler.startNested("Obtaining client");
DockerClient dockerClient = DockerClientFactory.instance().client();
try {
nested.stop();
profiler.start("Check local images");
int attempts = 0;
while (true) {
// Does our cache already know the image?
if (AVAILABLE_IMAGE_NAME_CACHE.contains(dockerImageName)) {
logger.trace("{} is already in image name cache", dockerImageName);
break;
}
// Update the cache
ListImagesCmd listImagesCmd = dockerClient.listImagesCmd();
if (Boolean.parseBoolean(System.getProperty("useFilter"))) {
listImagesCmd = listImagesCmd.withImageNameFilter(dockerImageName);
}
List<Image> updatedImages = listImagesCmd.exec();
for (Image image : updatedImages) {
if (image.getRepoTags() != null) {
Collections.addAll(AVAILABLE_IMAGE_NAME_CACHE, image.getRepoTags());
}
}
// And now?
if (AVAILABLE_IMAGE_NAME_CACHE.contains(dockerImageName)) {
logger.trace("{} is in image name cache following listing of images", dockerImageName);
break;
}
// Log only on first attempt
if (attempts == 0) {
logger.info("Pulling docker image: {}. Please be patient; this may take some time but only needs to be done once.", dockerImageName);
profiler.start("Pull image");
}
if (attempts++ >= 3) {
logger.error("Retry limit reached while trying to pull image: " + dockerImageName + ". Please check output of `docker pull " + dockerImageName + "`");
throw new ContainerFetchException("Retry limit reached while trying to pull image: " + dockerImageName);
}
// The image is not available locally - pull it
try {
dockerClient.pullImageCmd(dockerImageName).exec(new PullImageResultCallback()).awaitCompletion();
} catch (InterruptedException e) {
throw new ContainerFetchException("Failed to fetch container image for " + dockerImageName, e);
}
// Do not break here, but step into the next iteration, where it will be verified with listImagesCmd().
// see https://github.com/docker/docker/issues/10708
}
return dockerImageName;
} catch (DockerClientException e) {
throw new ContainerFetchException("Failed to get Docker client for " + dockerImageName, e);
} finally {
profiler.stop().log();
}
}
}