diff --git a/core/src/main/java/org/testcontainers/images/LocalImagesCache.java b/core/src/main/java/org/testcontainers/images/LocalImagesCache.java index c98339e9f4c..9c601959e88 100644 --- a/core/src/main/java/org/testcontainers/images/LocalImagesCache.java +++ b/core/src/main/java/org/testcontainers/images/LocalImagesCache.java @@ -70,22 +70,37 @@ private synchronized boolean maybeInitCache(DockerClient dockerClient) { return true; } - private void populateFromList(List images) { + @VisibleForTesting + void populateFromList(List images) { for (Image image : images) { String[] repoTags = image.getRepoTags(); - if (repoTags == null) { - log.debug("repoTags is null, skipping image: {}", image); + String[] repoDigests = image.getRepoDigests(); + + if (repoTags == null && repoDigests == null) { + log.debug("repoTags and repoDigests are both null, skipping image: {}", image); continue; } - cache.putAll( - Stream - .of(repoTags) - // Protection against some edge case where local image repository tags end up with duplicates - // making toMap crash at merge time. - .distinct() - .collect(Collectors.toMap(DockerImageName::new, it -> ImageData.from(image))) - ); + // Images pulled by digest only (e.g. "postgres@sha256:...") have no repo tags, + // so they must be indexed by their repo digests to be found in the cache. + putAll(repoTags, image); + putAll(repoDigests, image); + } + } + + private void putAll(String[] names, Image image) { + if (names == null) { + return; } + cache.putAll( + Stream + .of(names) + // Dangling images are reported as ":" / "@"; they can't be looked up by name. + .filter(it -> !it.startsWith("")) + // Protection against some edge case where local image repository tags end up with duplicates + // making toMap crash at merge time. + .distinct() + .collect(Collectors.toMap(DockerImageName::new, it -> ImageData.from(image))) + ); } } diff --git a/core/src/test/java/org/testcontainers/images/LocalImagesCacheTest.java b/core/src/test/java/org/testcontainers/images/LocalImagesCacheTest.java new file mode 100644 index 00000000000..42a1b1ebaef --- /dev/null +++ b/core/src/test/java/org/testcontainers/images/LocalImagesCacheTest.java @@ -0,0 +1,56 @@ +package org.testcontainers.images; + +import com.github.dockerjava.api.model.Image; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.testcontainers.utility.DockerImageName; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; + +class LocalImagesCacheTest { + + private static final String DIGEST = + "postgres@sha256:0000000000000000000000000000000000000000000000000000000000000001"; + + @AfterEach + void tearDown() { + LocalImagesCacheAccessor.clearCache(); + } + + @Test + void shouldCacheImagePulledByDigestOnly() { + Image image = Mockito.mock(Image.class); + Mockito.when(image.getRepoTags()).thenReturn(null); + Mockito.when(image.getRepoDigests()).thenReturn(new String[] { DIGEST }); + + LocalImagesCache.INSTANCE.populateFromList(Collections.singletonList(image)); + + assertThat(LocalImagesCache.INSTANCE.cache).containsKey(new DockerImageName(DIGEST)); + } + + @Test + void shouldCacheImageByTagAndDigest() { + Image image = Mockito.mock(Image.class); + Mockito.when(image.getRepoTags()).thenReturn(new String[] { "postgres:16" }); + Mockito.when(image.getRepoDigests()).thenReturn(new String[] { DIGEST }); + + LocalImagesCache.INSTANCE.populateFromList(Collections.singletonList(image)); + + assertThat(LocalImagesCache.INSTANCE.cache) + .containsKeys(new DockerImageName("postgres:16"), new DockerImageName(DIGEST)); + } + + @Test + void shouldSkipImageWithoutTagsAndDigests() { + Image image = Mockito.mock(Image.class); + Mockito.when(image.getRepoTags()).thenReturn(null); + Mockito.when(image.getRepoDigests()).thenReturn(null); + + LocalImagesCache.INSTANCE.populateFromList(Collections.singletonList(image)); + + assertThat(LocalImagesCache.INSTANCE.cache).isEmpty(); + } +}