Skip to content

Commit 26be859

Browse files
askfortianjie
andauthored
Make DockerComposeContainer only pull necessary images when multiple compose files were given (#3787)
Co-authored-by: tianjie <[email protected]>
1 parent b635eb5 commit 26be859

File tree

7 files changed

+127
-17
lines changed

7 files changed

+127
-17
lines changed

core/src/main/java/org/testcontainers/containers/DockerComposeContainer.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public class DockerComposeContainer<SELF extends DockerComposeContainer<SELF>> e
7777
*/
7878
private final String identifier;
7979
private final List<File> composeFiles;
80-
private Set<ParsedDockerComposeFile> parsedComposeFiles;
80+
private DockerComposeFiles dockerComposeFiles;
8181
private final Map<String, Integer> scalingPreferences = new HashMap<>();
8282
private DockerClient dockerClient;
8383
private boolean localCompose;
@@ -126,7 +126,7 @@ public DockerComposeContainer(String identifier, File... composeFiles) {
126126
public DockerComposeContainer(String identifier, List<File> composeFiles) {
127127

128128
this.composeFiles = composeFiles;
129-
this.parsedComposeFiles = composeFiles.stream().map(ParsedDockerComposeFile::new).collect(Collectors.toSet());
129+
this.dockerComposeFiles = new DockerComposeFiles(composeFiles);
130130

131131
// Use a unique identifier so that containers created for this compose environment can be identified
132132
this.identifier = identifier;
@@ -184,8 +184,7 @@ private void pullImages() {
184184
// Pull images using our docker client rather than compose itself,
185185
// (a) as a workaround for https://github.com/docker/compose/issues/5854, which prevents authenticated image pulls being possible when credential helpers are in use
186186
// (b) so that credential helper-based auth still works when compose is running from within a container
187-
parsedComposeFiles.stream()
188-
.flatMap(it -> it.getDependencyImageNames().stream())
187+
dockerComposeFiles.getDependencyImages()
189188
.forEach(imageName -> {
190189
try {
191190
log.info("Preemptively checking local images for '{}', referenced via a compose file or transitive Dockerfile. If not available, it will be pulled.", imageName);
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package org.testcontainers.containers;
2+
3+
import java.io.File;
4+
import java.util.HashMap;
5+
import java.util.HashSet;
6+
import java.util.List;
7+
import java.util.Map;
8+
import java.util.Map.Entry;
9+
import java.util.Set;
10+
import java.util.stream.Collectors;
11+
12+
public class DockerComposeFiles {
13+
14+
private List<ParsedDockerComposeFile> parsedComposeFiles;
15+
16+
public DockerComposeFiles(List<File> composeFiles) {
17+
this.parsedComposeFiles = composeFiles.stream().map(ParsedDockerComposeFile::new).collect(Collectors.toList());
18+
}
19+
20+
public Set<String> getDependencyImages() {
21+
22+
Map<String, Set<String>> mergedServiceNameToImageNames = mergeServiceDependencyImageNames();
23+
24+
return getImageNames(mergedServiceNameToImageNames);
25+
}
26+
27+
private Map<String, Set<String>> mergeServiceDependencyImageNames() {
28+
Map<String, Set<String>> mergedServiceNameToImageNames = new HashMap();
29+
for (ParsedDockerComposeFile parsedComposeFile : parsedComposeFiles) {
30+
for (Entry<String, Set<String>> entry : parsedComposeFile.getServiceNameToImageNames().entrySet()) {
31+
mergedServiceNameToImageNames.put(entry.getKey(), entry.getValue());
32+
}
33+
}
34+
return mergedServiceNameToImageNames;
35+
}
36+
37+
private Set<String> getImageNames(Map<String, Set<String>> serviceToImageNames) {
38+
Set<String> imageNames = new HashSet<>();
39+
serviceToImageNames.values().stream().forEach(imageNames::addAll);
40+
return imageNames;
41+
}
42+
43+
}

core/src/main/java/org/testcontainers/containers/ParsedDockerComposeFile.java

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.testcontainers.containers;
22

33
import com.google.common.annotations.VisibleForTesting;
4+
import com.google.common.collect.Sets;
45
import lombok.EqualsAndHashCode;
56
import lombok.Getter;
67
import lombok.extern.slf4j.Slf4j;
@@ -12,7 +13,7 @@
1213
import java.io.FileInputStream;
1314
import java.nio.file.Files;
1415
import java.nio.file.Path;
15-
import java.util.HashSet;
16+
import java.util.HashMap;
1617
import java.util.Map;
1718
import java.util.Set;
1819

@@ -29,7 +30,7 @@ class ParsedDockerComposeFile {
2930
private final File composeFile;
3031

3132
@Getter
32-
private Set<String> dependencyImageNames = new HashSet<>();
33+
private Map<String, Set<String>> serviceNameToImageNames = new HashMap<>();
3334

3435
ParsedDockerComposeFile(File composeFile) {
3536
Yaml yaml = new Yaml();
@@ -87,8 +88,8 @@ private void parseAndValidate() {
8788
final Map serviceDefinitionMap = (Map) serviceDefinition;
8889

8990
validateNoContainerNameSpecified(serviceName, serviceDefinitionMap);
90-
findServiceImageName(serviceDefinitionMap);
91-
findImageNamesInDockerfile(serviceDefinitionMap);
91+
findServiceImageName(serviceName, serviceDefinitionMap);
92+
findImageNamesInDockerfile(serviceName, serviceDefinitionMap);
9293
}
9394
}
9495

@@ -102,15 +103,15 @@ private void validateNoContainerNameSpecified(String serviceName, Map serviceDef
102103
}
103104
}
104105

105-
private void findServiceImageName(Map serviceDefinitionMap) {
106+
private void findServiceImageName(String serviceName, Map serviceDefinitionMap) {
106107
if (serviceDefinitionMap.containsKey("image") && serviceDefinitionMap.get("image") instanceof String) {
107108
final String imageName = (String) serviceDefinitionMap.get("image");
108109
log.debug("Resolved dependency image for Docker Compose in {}: {}", composeFileName, imageName);
109-
dependencyImageNames.add(imageName);
110+
serviceNameToImageNames.put(serviceName, Sets.newHashSet(imageName));
110111
}
111112
}
112113

113-
private void findImageNamesInDockerfile(Map serviceDefinitionMap) {
114+
private void findImageNamesInDockerfile(String serviceName, Map serviceDefinitionMap) {
114115
final Object buildNode = serviceDefinitionMap.get("build");
115116
Path dockerfilePath = null;
116117

@@ -141,7 +142,7 @@ private void findImageNamesInDockerfile(Map serviceDefinitionMap) {
141142
Set<String> resolvedImageNames = new ParsedDockerfile(dockerfilePath).getDependencyImageNames();
142143
if (!resolvedImageNames.isEmpty()) {
143144
log.debug("Resolved Dockerfile dependency images for Docker Compose in {} -> {}: {}", composeFileName, dockerfilePath, resolvedImageNames);
144-
this.dependencyImageNames.addAll(resolvedImageNames);
145+
this.serviceNameToImageNames.put(serviceName, resolvedImageNames);
145146
}
146147
}
147148
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.testcontainers.containers;
2+
3+
4+
import com.google.common.collect.Lists;
5+
import java.io.File;
6+
import org.assertj.core.api.Assertions;
7+
import org.junit.Test;
8+
9+
public class DockerComposeFilesTest {
10+
@Test
11+
public void shouldGetDependencyImages() {
12+
DockerComposeFiles dockerComposeFiles = new DockerComposeFiles(Lists.newArrayList(new File("src/test/resources/docker-compose-imagename-parsing-v2.yml")));
13+
Assertions.assertThat(dockerComposeFiles.getDependencyImages())
14+
.containsExactlyInAnyOrder("postgres", "redis", "mysql");
15+
}
16+
17+
@Test
18+
public void shouldGetDependencyImagesWhenOverriding() {
19+
DockerComposeFiles dockerComposeFiles = new DockerComposeFiles(
20+
Lists.newArrayList(new File("src/test/resources/docker-compose-imagename-overriding-a.yml"),
21+
new File("src/test/resources/docker-compose-imagename-overriding-b.yml")));
22+
Assertions.assertThat(dockerComposeFiles.getDependencyImages())
23+
.containsExactlyInAnyOrder("alpine:3.2", "redis:b", "mysql:b", "aservice");
24+
}
25+
}

core/src/test/java/org/testcontainers/containers/ParsedDockerComposeFileValidationTest.java

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import static java.util.Collections.emptyList;
1111
import static java.util.Collections.emptyMap;
12-
import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals;
12+
import static org.assertj.core.api.Assertions.entry;
1313

1414
public class ParsedDockerComposeFileValidationTest {
1515

@@ -76,27 +76,47 @@ public void shouldIgnoreUnknownStructure() {
7676
public void shouldObtainImageNamesV1() {
7777
File file = new File("src/test/resources/docker-compose-imagename-parsing-v1.yml");
7878
ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file);
79-
assertEquals("all defined images are found", Sets.newHashSet("redis", "mysql", "postgres"), parsedFile.getDependencyImageNames()); // redis, mysql from compose file, postgres from Dockerfile build
79+
Assertions.assertThat(parsedFile.getServiceNameToImageNames())
80+
.contains(
81+
entry("mysql", Sets.newHashSet("mysql")),
82+
entry("redis", Sets.newHashSet("redis")),
83+
entry("custom", Sets.newHashSet("postgres")))
84+
.as("all defined images are found"); // redis, mysql from compose file, postgres from Dockerfile build
8085
}
8186

8287
@Test
8388
public void shouldObtainImageNamesV2() {
8489
File file = new File("src/test/resources/docker-compose-imagename-parsing-v2.yml");
8590
ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file);
86-
assertEquals("all defined images are found", Sets.newHashSet("redis", "mysql", "postgres"), parsedFile.getDependencyImageNames()); // redis, mysql from compose file, postgres from Dockerfile build
91+
Assertions.assertThat(parsedFile.getServiceNameToImageNames())
92+
.contains(
93+
entry("mysql", Sets.newHashSet("mysql")),
94+
entry("redis", Sets.newHashSet("redis")),
95+
entry("custom", Sets.newHashSet("postgres")))
96+
.as("all defined images are found");
8797
}
8898

8999
@Test
90100
public void shouldObtainImageFromDockerfileBuild() {
91101
File file = new File("src/test/resources/docker-compose-imagename-parsing-dockerfile.yml");
92102
ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file);
93-
assertEquals("all defined images are found", Sets.newHashSet("redis", "mysql", "alpine:3.2"), parsedFile.getDependencyImageNames()); // redis, mysql from compose file, alpine:3.2 from Dockerfile build
103+
Assertions.assertThat(parsedFile.getServiceNameToImageNames())
104+
.contains(
105+
entry("mysql", Sets.newHashSet("mysql")),
106+
entry("redis", Sets.newHashSet("redis")),
107+
entry("custom", Sets.newHashSet("alpine:3.2")))
108+
.as("all defined images are found"); // r/ redis, mysql from compose file, alpine:3.2 from Dockerfile build
94109
}
95110

96111
@Test
97112
public void shouldObtainImageFromDockerfileBuildWithContext() {
98113
File file = new File("src/test/resources/docker-compose-imagename-parsing-dockerfile-with-context.yml");
99114
ParsedDockerComposeFile parsedFile = new ParsedDockerComposeFile(file);
100-
assertEquals("all defined images are found", Sets.newHashSet("redis", "mysql", "alpine:3.2"), parsedFile.getDependencyImageNames()); // redis, mysql from compose file, alpine:3.2 from Dockerfile build
115+
Assertions.assertThat(parsedFile.getServiceNameToImageNames())
116+
.contains(
117+
entry("mysql", Sets.newHashSet("mysql")),
118+
entry("redis", Sets.newHashSet("redis")),
119+
entry("custom", Sets.newHashSet("alpine:3.2")))
120+
.as("all defined images are found"); // redis, mysql from compose file, alpine:3.2 from Dockerfile build
101121
}
102122
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
version: "2.1"
2+
services:
3+
aservice:
4+
image: aservice
5+
redis:
6+
image: redis:a
7+
mysql:
8+
image: mysql:a
9+
custom:
10+
build: .
11+
networks:
12+
custom_network: {}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
version: "2.1"
2+
services:
3+
redis:
4+
image: redis:b
5+
mysql:
6+
image: mysql:b
7+
custom:
8+
build: compose-dockerfile
9+
networks:
10+
custom_network: {}

0 commit comments

Comments
 (0)