diff --git a/core/src/main/java/org/testcontainers/images/LocalImagesCache.java b/core/src/main/java/org/testcontainers/images/LocalImagesCache.java index c98339e9f4c..98db887030d 100644 --- a/core/src/main/java/org/testcontainers/images/LocalImagesCache.java +++ b/core/src/main/java/org/testcontainers/images/LocalImagesCache.java @@ -70,17 +70,23 @@ 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; } + Stream tagNames = repoTags != null ? Stream.of(repoTags) : Stream.empty(); + Stream digestNames = repoDigests != null ? Stream.of(repoDigests) : Stream.empty(); + cache.putAll( Stream - .of(repoTags) + .concat(tagNames, digestNames) // Protection against some edge case where local image repository tags end up with duplicates // making toMap crash at merge time. .distinct() 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..ead6d2101b9 --- /dev/null +++ b/core/src/test/java/org/testcontainers/images/LocalImagesCacheTest.java @@ -0,0 +1,67 @@ +package org.testcontainers.images; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.dockerjava.api.model.Image; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.testcontainers.utility.DockerImageName; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class LocalImagesCacheTest { + + @AfterEach + void tearDown() { + LocalImagesCacheAccessor.clearCache(); + } + + @Test + void shouldIndexImageByRepoDigestEvenWhenRepoTagsIsNull() { + Image image = imageWith(null, new String[] { "example.com/my/image@sha256:" + "a".repeat(64) }); + + LocalImagesCache.INSTANCE.populateFromList(Collections.singletonList(image)); + + DockerImageName byDigest = DockerImageName.parse("example.com/my/image@sha256:" + "a".repeat(64)); + assertThat(LocalImagesCache.INSTANCE.cache).containsKey(byDigest); + } + + @Test + void shouldIndexImageByBothRepoTagsAndRepoDigests() { + Image image = imageWith( + new String[] { "example.com/my/image:latest" }, + new String[] { "example.com/my/image@sha256:" + "b".repeat(64) } + ); + + LocalImagesCache.INSTANCE.populateFromList(Collections.singletonList(image)); + + assertThat(LocalImagesCache.INSTANCE.cache) + .containsKey(DockerImageName.parse("example.com/my/image:latest")) + .containsKey(DockerImageName.parse("example.com/my/image@sha256:" + "b".repeat(64))); + } + + @Test + void shouldSkipImageWithNeitherRepoTagsNorRepoDigests() { + Image image = imageWith(null, null); + + LocalImagesCache.INSTANCE.populateFromList(Collections.singletonList(image)); + + assertThat(LocalImagesCache.INSTANCE.cache).isEmpty(); + } + + private static Image imageWith(String[] repoTags, String[] repoDigests) { + Map fields = new HashMap<>(); + fields.put("Id", "sha256:" + "c".repeat(64)); + if (repoTags != null) { + fields.put("RepoTags", Arrays.asList(repoTags)); + } + if (repoDigests != null) { + fields.put("RepoDigests", Arrays.asList(repoDigests)); + } + return new ObjectMapper().convertValue(fields, Image.class); + } +}