Skip to content

Commit bf4e967

Browse files
committed
Support SHA256 digests and Image IDs in image cache lookup
Previously, LocalImagesCache only mapped RepoTags from Docker image metadata when caching locally available images. Images referenced by SHA256 digests or raw Image IDs (or images lacking repository tags) caused cache misses and triggered redundant pull attempts. Update LocalImagesCache to populate the cache using RepoDigests and Image IDs in addition to RepoTags, filtering out placeholder values such as <none>:<none> and <none>@<none>. Fixes #1406
1 parent a4d3a03 commit bf4e967

2 files changed

Lines changed: 124 additions & 17 deletions

File tree

‎core/src/main/java/org/testcontainers/images/LocalImagesCache.java‎

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
import java.util.Optional;
1515
import java.util.concurrent.ConcurrentHashMap;
1616
import java.util.concurrent.atomic.AtomicBoolean;
17-
import java.util.stream.Collectors;
18-
import java.util.stream.Stream;
1917

2018
@Slf4j
2119
enum LocalImagesCache {
@@ -35,8 +33,6 @@ public ImageData get(DockerImageName imageName) {
3533
public Optional<ImageData> refreshCache(DockerImageName imageName) {
3634
DockerClient dockerClient = DockerClientFactory.instance().client();
3735
if (!maybeInitCache(dockerClient)) {
38-
// Cache may be stale, trying inspectImageCmd...
39-
4036
InspectImageResponse response = null;
4137
try {
4238
response = dockerClient.inspectImageCmd(imageName.asCanonicalNameString()).exec();
@@ -46,6 +42,24 @@ public Optional<ImageData> refreshCache(DockerImageName imageName) {
4642
if (response != null) {
4743
ImageData imageData = ImageData.from(response);
4844
cache.put(imageName, imageData);
45+
if (response.getRepoDigests() != null) {
46+
for (String repoDigest : response.getRepoDigests()) {
47+
if (repoDigest != null && !"<none>@<none>".equals(repoDigest)) {
48+
try {
49+
cache.put(DockerImageName.parse(repoDigest), imageData);
50+
} catch (IllegalArgumentException ignored) {}
51+
}
52+
}
53+
}
54+
String imageId = response.getId();
55+
if (imageId != null) {
56+
try {
57+
cache.put(DockerImageName.parse(imageId), imageData);
58+
if (imageId.startsWith("sha256:")) {
59+
cache.put(DockerImageName.parse(imageId.substring(7)), imageData);
60+
}
61+
} catch (IllegalArgumentException ignored) {}
62+
}
4963
return Optional.of(imageData);
5064
} else {
5165
cache.remove(imageName);
@@ -56,7 +70,8 @@ public Optional<ImageData> refreshCache(DockerImageName imageName) {
5670
return Optional.ofNullable(cache.get(imageName));
5771
}
5872

59-
private synchronized boolean maybeInitCache(DockerClient dockerClient) {
73+
@VisibleForTesting
74+
synchronized boolean maybeInitCache(DockerClient dockerClient) {
6075
if (!initialized.compareAndSet(false, true)) {
6176
return false;
6277
}
@@ -72,20 +87,43 @@ private synchronized boolean maybeInitCache(DockerClient dockerClient) {
7287

7388
private void populateFromList(List<Image> images) {
7489
for (Image image : images) {
75-
String[] repoTags = image.getRepoTags();
76-
if (repoTags == null) {
77-
log.debug("repoTags is null, skipping image: {}", image);
78-
continue;
90+
ImageData imageData = ImageData.from(image);
91+
92+
if (image.getRepoTags() != null) {
93+
for (String repoTag : image.getRepoTags()) {
94+
if (repoTag != null && !"<none>:<none>".equals(repoTag)) {
95+
try {
96+
cache.put(DockerImageName.parse(repoTag), imageData);
97+
} catch (IllegalArgumentException e) {
98+
log.debug("Failed to parse repoTag: {}", repoTag, e);
99+
}
100+
}
101+
}
79102
}
80103

81-
cache.putAll(
82-
Stream
83-
.of(repoTags)
84-
// Protection against some edge case where local image repository tags end up with duplicates
85-
// making toMap crash at merge time.
86-
.distinct()
87-
.collect(Collectors.toMap(DockerImageName::new, it -> ImageData.from(image)))
88-
);
104+
if (image.getRepoDigests() != null) {
105+
for (String repoDigest : image.getRepoDigests()) {
106+
if (repoDigest != null && !"<none>@<none>".equals(repoDigest)) {
107+
try {
108+
cache.put(DockerImageName.parse(repoDigest), imageData);
109+
} catch (IllegalArgumentException e) {
110+
log.debug("Failed to parse repoDigest: {}", repoDigest, e);
111+
}
112+
}
113+
}
114+
}
115+
116+
String imageId = image.getId();
117+
if (imageId != null) {
118+
try {
119+
cache.put(DockerImageName.parse(imageId), imageData);
120+
if (imageId.startsWith("sha256:")) {
121+
cache.put(DockerImageName.parse(imageId.substring(7)), imageData);
122+
}
123+
} catch (IllegalArgumentException e) {
124+
log.debug("Failed to parse image id: {}", imageId, e);
125+
}
126+
}
89127
}
90128
}
91129
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package org.testcontainers.images;
2+
3+
import com.github.dockerjava.api.DockerClient;
4+
import com.github.dockerjava.api.command.ListImagesCmd;
5+
import com.github.dockerjava.api.model.Image;
6+
import org.junit.jupiter.api.AfterEach;
7+
import org.junit.jupiter.api.BeforeEach;
8+
import org.junit.jupiter.api.Test;
9+
import org.mockito.Mockito;
10+
import org.testcontainers.utility.DockerImageName;
11+
12+
import java.util.Collections;
13+
14+
import static org.assertj.core.api.Assertions.assertThat;
15+
import static org.mockito.Mockito.when;
16+
17+
class LocalImagesCacheTest {
18+
19+
@BeforeEach
20+
@AfterEach
21+
void resetCache() {
22+
LocalImagesCacheAccessor.clearCache();
23+
}
24+
25+
@Test
26+
void shouldCacheRepoDigestsAndImageIds() {
27+
DockerClient dockerClient = Mockito.mock(DockerClient.class);
28+
ListImagesCmd listImagesCmd = Mockito.mock(ListImagesCmd.class);
29+
30+
when(dockerClient.listImagesCmd()).thenReturn(listImagesCmd);
31+
32+
Image image = Mockito.mock(Image.class);
33+
when(image.getRepoTags()).thenReturn(new String[] { "test-repo:1.0", "<none>:<none>" });
34+
when(image.getRepoDigests())
35+
.thenReturn(
36+
new String[] {
37+
"test-repo@sha256:e1594798e61a75abde649ed1432fa955853a7816f516fe49360d623213a01d96",
38+
"<none>@<none>",
39+
}
40+
);
41+
when(image.getId()).thenReturn("sha256:e1594798e61a75abde649ed1432fa955853a7816f516fe49360d623213a01d96");
42+
when(image.getCreated()).thenReturn(1595874211L);
43+
44+
when(listImagesCmd.exec()).thenReturn(Collections.singletonList(image));
45+
46+
LocalImagesCache.INSTANCE.maybeInitCache(dockerClient);
47+
48+
ImageData byTag = LocalImagesCache.INSTANCE.cache.get(DockerImageName.parse("test-repo:1.0"));
49+
assertThat(byTag).isNotNull();
50+
51+
ImageData byDigest = LocalImagesCache.INSTANCE.cache.get(
52+
DockerImageName.parse("test-repo@sha256:e1594798e61a75abde649ed1432fa955853a7816f516fe49360d623213a01d96")
53+
);
54+
assertThat(byDigest).isNotNull();
55+
56+
ImageData byIdWithPrefix = LocalImagesCache.INSTANCE.cache.get(
57+
DockerImageName.parse("sha256:e1594798e61a75abde649ed1432fa955853a7816f516fe49360d623213a01d96")
58+
);
59+
assertThat(byIdWithPrefix).isNotNull();
60+
61+
ImageData byIdWithoutPrefix = LocalImagesCache.INSTANCE.cache.get(
62+
DockerImageName.parse("e1594798e61a75abde649ed1432fa955853a7816f516fe49360d623213a01d96")
63+
);
64+
assertThat(byIdWithoutPrefix).isNotNull();
65+
66+
ImageData noneTag = LocalImagesCache.INSTANCE.cache.get(DockerImageName.parse("<none>:<none>"));
67+
assertThat(noneTag).isNull();
68+
}
69+
}

0 commit comments

Comments
 (0)