diff --git a/core/src/main/java/org/testcontainers/images/LocalImagesCache.java b/core/src/main/java/org/testcontainers/images/LocalImagesCache.java index c98339e9f4c..53139534247 100644 --- a/core/src/main/java/org/testcontainers/images/LocalImagesCache.java +++ b/core/src/main/java/org/testcontainers/images/LocalImagesCache.java @@ -14,8 +14,6 @@ import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Collectors; -import java.util.stream.Stream; @Slf4j enum LocalImagesCache { @@ -35,8 +33,6 @@ public ImageData get(DockerImageName imageName) { public Optional refreshCache(DockerImageName imageName) { DockerClient dockerClient = DockerClientFactory.instance().client(); if (!maybeInitCache(dockerClient)) { - // Cache may be stale, trying inspectImageCmd... - InspectImageResponse response = null; try { response = dockerClient.inspectImageCmd(imageName.asCanonicalNameString()).exec(); @@ -46,6 +42,24 @@ public Optional refreshCache(DockerImageName imageName) { if (response != null) { ImageData imageData = ImageData.from(response); cache.put(imageName, imageData); + if (response.getRepoDigests() != null) { + for (String repoDigest : response.getRepoDigests()) { + if (repoDigest != null && !"@".equals(repoDigest)) { + try { + cache.put(DockerImageName.parse(repoDigest), imageData); + } catch (IllegalArgumentException ignored) {} + } + } + } + String imageId = response.getId(); + if (imageId != null) { + try { + cache.put(DockerImageName.parse(imageId), imageData); + if (imageId.startsWith("sha256:")) { + cache.put(DockerImageName.parse(imageId.substring(7)), imageData); + } + } catch (IllegalArgumentException ignored) {} + } return Optional.of(imageData); } else { cache.remove(imageName); @@ -56,7 +70,8 @@ public Optional refreshCache(DockerImageName imageName) { return Optional.ofNullable(cache.get(imageName)); } - private synchronized boolean maybeInitCache(DockerClient dockerClient) { + @VisibleForTesting + synchronized boolean maybeInitCache(DockerClient dockerClient) { if (!initialized.compareAndSet(false, true)) { return false; } @@ -72,20 +87,43 @@ private synchronized boolean maybeInitCache(DockerClient dockerClient) { private void populateFromList(List images) { for (Image image : images) { - String[] repoTags = image.getRepoTags(); - if (repoTags == null) { - log.debug("repoTags is null, skipping image: {}", image); - continue; + ImageData imageData = ImageData.from(image); + + if (image.getRepoTags() != null) { + for (String repoTag : image.getRepoTags()) { + if (repoTag != null && !":".equals(repoTag)) { + try { + cache.put(DockerImageName.parse(repoTag), imageData); + } catch (IllegalArgumentException e) { + log.debug("Failed to parse repoTag: {}", repoTag, e); + } + } + } } - 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))) - ); + if (image.getRepoDigests() != null) { + for (String repoDigest : image.getRepoDigests()) { + if (repoDigest != null && !"@".equals(repoDigest)) { + try { + cache.put(DockerImageName.parse(repoDigest), imageData); + } catch (IllegalArgumentException e) { + log.debug("Failed to parse repoDigest: {}", repoDigest, e); + } + } + } + } + + String imageId = image.getId(); + if (imageId != null) { + try { + cache.put(DockerImageName.parse(imageId), imageData); + if (imageId.startsWith("sha256:")) { + cache.put(DockerImageName.parse(imageId.substring(7)), imageData); + } + } catch (IllegalArgumentException e) { + log.debug("Failed to parse image id: {}", imageId, e); + } + } } } } 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..987a838731c --- /dev/null +++ b/core/src/test/java/org/testcontainers/images/LocalImagesCacheTest.java @@ -0,0 +1,69 @@ +package org.testcontainers.images; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.ListImagesCmd; +import com.github.dockerjava.api.model.Image; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +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; +import static org.mockito.Mockito.when; + +class LocalImagesCacheTest { + + @BeforeEach + @AfterEach + void resetCache() { + LocalImagesCacheAccessor.clearCache(); + } + + @Test + void shouldCacheRepoDigestsAndImageIds() { + DockerClient dockerClient = Mockito.mock(DockerClient.class); + ListImagesCmd listImagesCmd = Mockito.mock(ListImagesCmd.class); + + when(dockerClient.listImagesCmd()).thenReturn(listImagesCmd); + + Image image = Mockito.mock(Image.class); + when(image.getRepoTags()).thenReturn(new String[] { "test-repo:1.0", ":" }); + when(image.getRepoDigests()) + .thenReturn( + new String[] { + "test-repo@sha256:e1594798e61a75abde649ed1432fa955853a7816f516fe49360d623213a01d96", + "@", + } + ); + when(image.getId()).thenReturn("sha256:e1594798e61a75abde649ed1432fa955853a7816f516fe49360d623213a01d96"); + when(image.getCreated()).thenReturn(1595874211L); + + when(listImagesCmd.exec()).thenReturn(Collections.singletonList(image)); + + LocalImagesCache.INSTANCE.maybeInitCache(dockerClient); + + ImageData byTag = LocalImagesCache.INSTANCE.cache.get(DockerImageName.parse("test-repo:1.0")); + assertThat(byTag).isNotNull(); + + ImageData byDigest = LocalImagesCache.INSTANCE.cache.get( + DockerImageName.parse("test-repo@sha256:e1594798e61a75abde649ed1432fa955853a7816f516fe49360d623213a01d96") + ); + assertThat(byDigest).isNotNull(); + + ImageData byIdWithPrefix = LocalImagesCache.INSTANCE.cache.get( + DockerImageName.parse("sha256:e1594798e61a75abde649ed1432fa955853a7816f516fe49360d623213a01d96") + ); + assertThat(byIdWithPrefix).isNotNull(); + + ImageData byIdWithoutPrefix = LocalImagesCache.INSTANCE.cache.get( + DockerImageName.parse("e1594798e61a75abde649ed1432fa955853a7816f516fe49360d623213a01d96") + ); + assertThat(byIdWithoutPrefix).isNotNull(); + + ImageData noneTag = LocalImagesCache.INSTANCE.cache.get(DockerImageName.parse(":")); + assertThat(noneTag).isNull(); + } +}