diff --git a/core/src/main/java/org/testcontainers/containers/GenericContainer.java b/core/src/main/java/org/testcontainers/containers/GenericContainer.java index 2b3abc594f7..fde8fa383d8 100644 --- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java +++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java @@ -9,12 +9,12 @@ import com.github.dockerjava.api.command.LogContainerCmd; import com.github.dockerjava.api.model.*; import com.github.dockerjava.core.async.ResultCallbackTemplate; -import com.github.dockerjava.core.command.PullImageResultCallback; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NonNull; +import lombok.SneakyThrows; import org.jetbrains.annotations.Nullable; import org.junit.runner.Description; import org.rnorth.ducttape.TimeoutException; @@ -36,6 +36,8 @@ import java.net.URL; import java.nio.file.Path; import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -63,7 +65,7 @@ public class GenericContainer extends FailureDetectingExternalResource implement private List portBindings = new ArrayList<>(); @NonNull - private String dockerImageName = "alpine:3.2"; + private Future image; @NonNull private List env = new ArrayList<>(); @@ -99,13 +101,17 @@ public class GenericContainer extends FailureDetectingExternalResource implement .build(); public GenericContainer() { - + this("alpine:3.2"); } public GenericContainer(@NonNull final String dockerImageName) { this.setDockerImageName(dockerImageName); } + public GenericContainer(@NonNull final Future image) { + this.image = image; + } + /** * Starts the container using docker, pulling an image if necessary. */ @@ -113,7 +119,7 @@ public void start() { int[] attempt = {0}; Unreliables.retryUntilSuccess(STARTUP_RETRY_COUNT, () -> { attempt[0]++; - logger().debug("Trying to start container: {} (attempt {}/{})", dockerImageName, attempt[0], STARTUP_RETRY_COUNT); + logger().debug("Trying to start container: {} (attempt {}/{})", image.get(), attempt[0], STARTUP_RETRY_COUNT); this.tryStart(); return true; }); @@ -123,11 +129,9 @@ private void tryStart() { Profiler profiler = new Profiler("Container startup"); profiler.setLogger(logger()); - logger().debug("Starting container: {}", dockerImageName); - try { - - pullImageIfNeeded(dockerImageName, profiler.startNested("Pull image if needed")); + String dockerImageName = image.get(); + logger().debug("Starting container: {}", dockerImageName); profiler.start("Prepare container configuration and host configuration"); configure(); @@ -196,7 +200,7 @@ public void stop() { try { logger().trace("Stopping container: {}", containerId); dockerClient.killContainerCmd(containerId).exec(); - logger().info("Stopped container: {}", dockerImageName); + logger().info("Stopped container: {}", containerId); } catch (DockerException e) { logger().trace("Error encountered shutting down container (ID: {}) - it may not have been stopped, or may already be stopped: {}", containerId, e.getMessage()); } @@ -205,9 +209,9 @@ public void stop() { logger().trace("Stopping container: {}", containerId); try { dockerClient.removeContainerCmd(containerId).withRemoveVolumes(true).withForce(true).exec(); - logger().info("Removed container and associated volume(s): {}", dockerImageName); + logger().info("Removed container and associated volume(s): {}", containerId); } catch (InternalServerErrorException e) { - logger().warn("Exception when removing container with associated volume(s): {} (due to {})", dockerImageName, e.getMessage()); + logger().warn("Exception when removing container with associated volume(s): {} (due to {})", containerId, e.getMessage()); } } catch (DockerException e) { logger().trace("Error encountered shutting down container (ID: {}) - it may not have been stopped, or may already be stopped: {}", containerId, e.getMessage()); @@ -221,52 +225,9 @@ public void stop() { */ protected Logger logger() { if ("UTF-8".equals(System.getProperty("file.encoding"))) { - return LoggerFactory.getLogger("\uD83D\uDC33 [" + dockerImageName + "]"); + return LoggerFactory.getLogger("\uD83D\uDC33 [" + this.getDockerImageName() + "]"); } else { - return LoggerFactory.getLogger("docker[" + dockerImageName + "]"); - } - } - - private void pullImageIfNeeded(final String imageName, Profiler profiler) throws DockerException, InterruptedException { - - // Does our cache already know the image? - if (AVAILABLE_IMAGE_NAME_CACHE.contains(imageName)) { - return; - } - - profiler.start("Check local images"); - // Update the cache - List images = dockerClient.listImagesCmd().exec(); - for (Image image : images) { - Collections.addAll(AVAILABLE_IMAGE_NAME_CACHE, image.getRepoTags()); - } - - // Check cache again following update - if (AVAILABLE_IMAGE_NAME_CACHE.contains(imageName)) { - return; - } - - logger().info("Pulling docker image: {}. Please be patient; this may take some time but only needs to be done once.", imageName); - profiler.start("Pull image"); - int attempts = 0; - while (!AVAILABLE_IMAGE_NAME_CACHE.contains(imageName)) { - // The image is not available locally - pull it - - if (attempts++ >= 3) { - logger().error("Retry limit reached while trying to pull image: " + imageName + ". Please check output of `docker pull " + imageName + "`"); - throw new ContainerFetchException("Retry limit reached while trying to pull image: " + imageName); - } - - PullImageResultCallback resultCallback = dockerClient.pullImageCmd(imageName).exec(new PullImageResultCallback()); - - resultCallback.awaitCompletion(); - - // Confirm successful pull, otherwise may need to retry... - // see https://github.com/docker/docker/issues/10708 - List updatedImages = dockerClient.listImagesCmd().exec(); - for (Image image : updatedImages) { - Collections.addAll(AVAILABLE_IMAGE_NAME_CACHE, image.getRepoTags()); - } + return LoggerFactory.getLogger("docker[" + this.getDockerImageName() + "]"); } } @@ -577,21 +538,41 @@ public Integer getMappedPort(final int originalPort) { } } + /** + * Resolve Docker image and set it. + * + * @param dockerImageName image name + */ + @SneakyThrows public void setDockerImageName(@NonNull String dockerImageName) { + String[] parts = dockerImageName.split(":"); + boolean isTagNamePresent = parts.length == 2; + Preconditions.checkArgument(isTagNamePresent, "No image tag was specified in docker image name (" + dockerImageName + "). Please provide a tag; this may be 'latest' or a specific version"); - this.dockerImageName = dockerImageName; + RemoteDockerImage remoteDockerImage = new RemoteDockerImage(parts[0], parts[1]); - boolean isTagNamePresent = dockerImageName.split(":").length == 2; - Preconditions.checkArgument(isTagNamePresent, "No image tag was specified in docker image name (" + dockerImageName + "). Please provide a tag; this may be 'latest' or a specific version"); + try { + // Mimic old behavior where we resolve image once it's set + remoteDockerImage.get(); + } catch (ExecutionException e) { + throw e.getCause(); + } - Profiler profiler = new Profiler("Rule creation - prefetch image"); - profiler.setLogger(logger()); + this.image = remoteDockerImage; + } + + /** + * Get image name. + * + * @return image name + */ + @NonNull + @SneakyThrows + public String getDockerImageName() { try { - pullImageIfNeeded(dockerImageName, profiler); - } catch (InterruptedException e) { - throw new ContainerFetchException("Failed to fetch container image for " + dockerImageName, e); - } finally { - profiler.stop().log(); + return image.get(); + } catch (ExecutionException e) { + throw e.getCause(); } } diff --git a/core/src/main/java/org/testcontainers/containers/RemoteDockerImage.java b/core/src/main/java/org/testcontainers/containers/RemoteDockerImage.java new file mode 100644 index 00000000000..8373ea20a65 --- /dev/null +++ b/core/src/main/java/org/testcontainers/containers/RemoteDockerImage.java @@ -0,0 +1,92 @@ +package org.testcontainers.containers; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.model.Image; +import com.github.dockerjava.core.command.PullImageResultCallback; +import com.google.common.util.concurrent.Futures; +import lombok.NonNull; +import lombok.experimental.Delegate; +import org.slf4j.LoggerFactory; +import org.slf4j.profiler.Profiler; +import org.testcontainers.DockerClientFactory; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; + +public class RemoteDockerImage implements Future { + + private static final Set AVAILABLE_IMAGE_NAME_CACHE = new HashSet<>(); + + @Delegate + private final Future delegate; + + public RemoteDockerImage(@NonNull String repository, String tag) { + delegate = Futures.lazyTransform(CompletableFuture.completedFuture(repository + ":" + tag), dockerImageName -> { + + Profiler profiler = new Profiler("Rule creation - prefetch image"); + + if ("UTF-8".equals(System.getProperty("file.encoding"))) { + profiler.setLogger(LoggerFactory.getLogger("\uD83D\uDC33 [" + dockerImageName + "]")); + } else { + profiler.setLogger(LoggerFactory.getLogger("docker[" + dockerImageName + "]")); + } + + try (DockerClient dockerClient = DockerClientFactory.instance().client()) { + + profiler.start("Check local images"); + + int attempts = 0; + while (true) { + // Does our cache already know the image? + if (AVAILABLE_IMAGE_NAME_CACHE.contains(dockerImageName)) { + break; + } + + // Update the cache + List updatedImages = dockerClient.listImagesCmd().exec(); + for (Image image : updatedImages) { + Collections.addAll(AVAILABLE_IMAGE_NAME_CACHE, image.getRepoTags()); + } + + // And now? + if (AVAILABLE_IMAGE_NAME_CACHE.contains(dockerImageName)) { + break; + } + + // Log only on first attempt + if (attempts == 0) { + profiler.getLogger().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) { + profiler.getLogger().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 (IOException e) { + throw new ContainerFetchException("Failed to get Docker client for " + dockerImageName, e); + } finally { + profiler.stop().log(); + } + }); + } + +}