Skip to content

Commit 53e02a5

Browse files
rnorthmnafshin
andauthored
Run correct subset of docker compose containers when withServices/withScaledService used (#2922)
Co-authored-by: Afshin <[email protected]>
1 parent 3f64d0c commit 53e02a5

File tree

4 files changed

+184
-50
lines changed

4 files changed

+184
-50
lines changed

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

Lines changed: 65 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
import com.github.dockerjava.api.DockerClient;
44
import com.github.dockerjava.api.model.Container;
5+
import com.google.common.annotations.VisibleForTesting;
56
import com.google.common.base.Joiner;
67
import com.google.common.base.Splitter;
7-
import com.google.common.base.Strings;
88
import com.google.common.collect.Maps;
99
import com.google.common.collect.Sets;
1010
import com.google.common.util.concurrent.Uninterruptibles;
@@ -56,6 +56,7 @@
5656

5757
import static com.google.common.base.Preconditions.checkArgument;
5858
import static com.google.common.base.Preconditions.checkNotNull;
59+
import static com.google.common.base.Strings.isNullOrEmpty;
5960
import static java.util.stream.Collectors.joining;
6061
import static java.util.stream.Collectors.toList;
6162
import static org.testcontainers.containers.BindMode.READ_ONLY;
@@ -196,23 +197,36 @@ public SELF withServices(@NonNull String... services) {
196197
}
197198

198199
private void createServices() {
199-
// Apply scaling
200-
final String servicesWithScalingSettings = Stream.concat(services.stream(), scalingPreferences.keySet().stream())
201-
.map(service -> "--scale " + service + "=" + scalingPreferences.getOrDefault(service, 1))
200+
// services that have been explicitly requested to be started. If empty, all services should be started.
201+
final String serviceNameArgs = Stream.concat(
202+
services.stream(), // services that have been specified with `withServices`
203+
scalingPreferences.keySet().stream() // services that are implicitly needed via `withScaledService`
204+
)
205+
.distinct()
202206
.collect(joining(" "));
203207

204-
String flags = "-d";
208+
// Apply scaling for the services specified using `withScaledService`
209+
final String scalingOptions = scalingPreferences.entrySet().stream()
210+
.map(entry -> "--scale " + entry.getKey() + "=" + entry.getValue())
211+
.distinct()
212+
.collect(joining(" "));
213+
214+
String command = "up -d";
205215

206216
if (build) {
207-
flags += " --build";
217+
command += " --build";
208218
}
209219

210-
// Run the docker-compose container, which starts up the services
211-
if(Strings.isNullOrEmpty(servicesWithScalingSettings)) {
212-
runWithCompose("up " + flags);
213-
} else {
214-
runWithCompose("up " + flags + " " + servicesWithScalingSettings);
220+
if (!isNullOrEmpty(scalingOptions)) {
221+
command += " " + scalingOptions;
222+
}
223+
224+
if (!isNullOrEmpty(serviceNameArgs)) {
225+
command += " " + serviceNameArgs;
215226
}
227+
228+
// Run the docker-compose container, which starts up the services
229+
runWithCompose(command);
216230
}
217231

218232
private void waitUntilServiceStarted() {
@@ -250,7 +264,7 @@ private void createServiceInstance(Container container) {
250264

251265
private void waitUntilServiceStarted(String serviceName, ComposeServiceWaitStrategyTarget serviceInstance) {
252266
final WaitAllStrategy waitAllStrategy = waitStrategyMap.get(serviceName);
253-
if(waitAllStrategy != null) {
267+
if (waitAllStrategy != null) {
254268
waitAllStrategy.waitUntilReady(serviceInstance);
255269
}
256270
}
@@ -273,24 +287,25 @@ private void runWithCompose(String cmd) {
273287
}
274288

275289
dockerCompose
276-
.withCommand(cmd)
277-
.withEnv(env)
278-
.invoke();
290+
.withCommand(cmd)
291+
.withEnv(env)
292+
.invoke();
279293
}
280294

281295
private void registerContainersForShutdown() {
282296
ResourceReaper.instance().registerFilterForCleanup(Arrays.asList(
283-
new SimpleEntry<>("label", "com.docker.compose.project=" + project)
297+
new SimpleEntry<>("label", "com.docker.compose.project=" + project)
284298
));
285299
}
286300

287-
private List<Container> listChildContainers() {
301+
@VisibleForTesting
302+
List<Container> listChildContainers() {
288303
return dockerClient.listContainersCmd()
289-
.withShowAll(true)
290-
.exec().stream()
291-
.filter(container -> Arrays.stream(container.getNames()).anyMatch(name ->
292-
name.startsWith("/" + project)))
293-
.collect(toList());
304+
.withShowAll(true)
305+
.exec().stream()
306+
.filter(container -> Arrays.stream(container.getNames()).anyMatch(name ->
307+
name.startsWith("/" + project)))
308+
.collect(toList());
294309
}
295310

296311
private void startAmbassadorContainers() {
@@ -378,12 +393,12 @@ private void addWaitStrategy(String serviceInstanceName, @NonNull WaitStrategy w
378393
}
379394

380395
/**
381-
Specify the {@link WaitStrategy} to use to determine if the container is ready.
396+
* Specify the {@link WaitStrategy} to use to determine if the container is ready.
382397
*
383-
* @see org.testcontainers.containers.wait.strategy.Wait#defaultWaitStrategy()
384-
* @param serviceName the name of the service to wait for
398+
* @param serviceName the name of the service to wait for
385399
* @param waitStrategy the WaitStrategy to use
386400
* @return this
401+
* @see org.testcontainers.containers.wait.strategy.Wait#defaultWaitStrategy()
387402
*/
388403
public SELF waitingFor(String serviceName, @NonNull WaitStrategy waitStrategy) {
389404
String serviceInstanceName = getServiceInstanceName(serviceName);
@@ -420,8 +435,8 @@ public Integer getServicePort(String serviceName, Integer servicePort) {
420435

421436
if (portMap == null) {
422437
throw new IllegalArgumentException("Could not get a port for '" + serviceName + "'. " +
423-
"Testcontainers does not have an exposed port configured for '" + serviceName + "'. "+
424-
"To fix, please ensure that the service '" + serviceName + "' has ports exposed using .withExposedService(...)");
438+
"Testcontainers does not have an exposed port configured for '" + serviceName + "'. " +
439+
"To fix, please ensure that the service '" + serviceName + "' has ports exposed using .withExposedService(...)");
425440
} else {
426441
return ambassadorContainer.getMappedPort(portMap.get(servicePort));
427442
}
@@ -479,7 +494,7 @@ public SELF withTailChildContainers(boolean tailChildContainers) {
479494
* More than one consumer may be registered.
480495
*
481496
* @param serviceName the name of the service as set in the docker-compose.yml file
482-
* @param consumer consumer that output frames should be sent to
497+
* @param consumer consumer that output frames should be sent to
483498
* @return this instance, for chaining
484499
*/
485500
public SELF withLogConsumer(String serviceName, Consumer<OutputFrame> consumer) {
@@ -579,10 +594,10 @@ public ContainerisedDockerCompose(List<File> composeFiles, String identifier) {
579594
final String containerPwd = MountableFile.forHostPath(pwd).getFilesystemPath();
580595

581596
final List<String> absoluteDockerComposeFiles = composeFiles.stream()
582-
.map(File::getAbsolutePath)
583-
.map(MountableFile::forHostPath)
584-
.map(MountableFile::getFilesystemPath)
585-
.collect(toList());
597+
.map(File::getAbsolutePath)
598+
.map(MountableFile::forHostPath)
599+
.map(MountableFile::getFilesystemPath)
600+
.collect(toList());
586601
final String composeFileEnvVariableValue = Joiner.on(UNIX_PATH_SEPERATOR).join(absoluteDockerComposeFiles); // we always need the UNIX path separator
587602
logger().debug("Set env COMPOSE_FILE={}", composeFileEnvVariableValue);
588603
addEnv(ENV_COMPOSE_FILE, composeFileEnvVariableValue);
@@ -600,8 +615,8 @@ public ContainerisedDockerCompose(List<File> composeFiles, String identifier) {
600615

601616
private String getDockerSocketHostPath() {
602617
return SystemUtils.IS_OS_WINDOWS
603-
? "/" + DOCKER_SOCKET_PATH
604-
: DOCKER_SOCKET_PATH;
618+
? "/" + DOCKER_SOCKET_PATH
619+
: DOCKER_SOCKET_PATH;
605620
}
606621

607622
@Override
@@ -621,16 +636,16 @@ public void invoke() {
621636
AuditLogger.doComposeLog(this.getCommandParts(), this.getEnv());
622637

623638
final Integer exitCode = this.dockerClient.inspectContainerCmd(getContainerId())
624-
.exec()
625-
.getState()
626-
.getExitCode();
639+
.exec()
640+
.getState()
641+
.getExitCode();
627642

628643
if (exitCode == null || exitCode != 0) {
629644
throw new ContainerLaunchException(
630-
"Containerised Docker Compose exited abnormally with code " +
631-
exitCode +
632-
" whilst running command: " +
633-
StringUtils.join(this.getCommandParts(), ' '));
645+
"Containerised Docker Compose exited abnormally with code " +
646+
exitCode +
647+
" whilst running command: " +
648+
StringUtils.join(this.getCommandParts(), ' '));
634649
}
635650
}
636651
}
@@ -691,23 +706,23 @@ public void invoke() {
691706
logger().info("Local Docker Compose is running command: {}", cmd);
692707

693708
final List<String> command = Splitter.onPattern(" ")
694-
.omitEmptyStrings()
695-
.splitToList(COMPOSE_EXECUTABLE + " " + cmd);
709+
.omitEmptyStrings()
710+
.splitToList(COMPOSE_EXECUTABLE + " " + cmd);
696711

697712
try {
698713
new ProcessExecutor().command(command)
699-
.redirectOutput(Slf4jStream.of(logger()).asInfo())
700-
.redirectError(Slf4jStream.of(logger()).asInfo()) // docker-compose will log pull information to stderr
701-
.environment(environment)
702-
.directory(pwd)
703-
.exitValueNormal()
704-
.executeNoTimeout();
714+
.redirectOutput(Slf4jStream.of(logger()).asInfo())
715+
.redirectError(Slf4jStream.of(logger()).asInfo()) // docker-compose will log pull information to stderr
716+
.environment(environment)
717+
.directory(pwd)
718+
.exitValueNormal()
719+
.executeNoTimeout();
705720

706721
logger().info("Docker Compose has finished running");
707722

708723
} catch (InvalidExitValueException e) {
709724
throw new ContainerLaunchException("Local Docker Compose exited abnormally with code " +
710-
e.getExitValue() + " whilst running command: " + cmd);
725+
e.getExitValue() + " whilst running command: " + cmd);
711726

712727
} catch (Exception e) {
713728
throw new ContainerLaunchException("Error running local Docker Compose command: " + cmd, e);
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package org.testcontainers.containers;
2+
3+
import org.junit.Test;
4+
5+
import java.io.File;
6+
import java.util.List;
7+
import java.util.stream.Collectors;
8+
import java.util.stream.Stream;
9+
10+
import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals;
11+
12+
13+
public class DockerComposeContainerWithServicesTest {
14+
15+
public static final File SIMPLE_COMPOSE_FILE = new File("src/test/resources/compose-scaling-multiple-containers.yml");
16+
public static final File COMPOSE_FILE_WITH_INLINE_SCALE = new File("src/test/resources/compose-with-inline-scale-test.yml");
17+
18+
@Test
19+
public void testDesiredSubsetOfServicesAreStarted() {
20+
try (
21+
DockerComposeContainer<?> compose = new DockerComposeContainer<>(SIMPLE_COMPOSE_FILE)
22+
.withServices("redis")
23+
) {
24+
compose.start();
25+
26+
verifyStartedContainers(compose, "redis_1");
27+
}
28+
}
29+
30+
@Test
31+
public void testDesiredSubsetOfScaledServicesAreStarted() {
32+
try (
33+
DockerComposeContainer<?> compose = new DockerComposeContainer<>(SIMPLE_COMPOSE_FILE)
34+
.withScaledService("redis", 2)
35+
) {
36+
compose.start();
37+
38+
verifyStartedContainers(compose, "redis_1", "redis_2");
39+
}
40+
}
41+
42+
@Test
43+
public void testDesiredSubsetOfSpecifiedAndScaledServicesAreStarted() {
44+
try (
45+
DockerComposeContainer<?> compose = new DockerComposeContainer<>(SIMPLE_COMPOSE_FILE)
46+
.withServices("redis")
47+
.withScaledService("redis", 2)
48+
) {
49+
compose.start();
50+
51+
verifyStartedContainers(compose, "redis_1", "redis_2");
52+
}
53+
}
54+
55+
@Test
56+
public void testDesiredSubsetOfSpecifiedOrScaledServicesAreStarted() {
57+
try (
58+
DockerComposeContainer<?> compose = new DockerComposeContainer<>(SIMPLE_COMPOSE_FILE)
59+
.withServices("other")
60+
.withScaledService("redis", 2)
61+
) {
62+
compose.start();
63+
64+
verifyStartedContainers(compose, "redis_1", "redis_2", "other_1");
65+
}
66+
}
67+
68+
@Test
69+
public void testAllServicesAreStartedIfNotSpecified() {
70+
try (
71+
DockerComposeContainer<?> compose = new DockerComposeContainer<>(SIMPLE_COMPOSE_FILE)
72+
) {
73+
compose.start();
74+
75+
verifyStartedContainers(compose, "redis_1", "other_1");
76+
}
77+
}
78+
79+
@Test
80+
public void testScaleInComposeFileIsRespected() {
81+
try (
82+
DockerComposeContainer<?> compose = new DockerComposeContainer<>(COMPOSE_FILE_WITH_INLINE_SCALE)
83+
) {
84+
compose.start();
85+
86+
// the compose file includes `scale: 3` for the redis container
87+
verifyStartedContainers(compose, "redis_1", "redis_2", "redis_3");
88+
}
89+
}
90+
91+
private void verifyStartedContainers(final DockerComposeContainer<?> compose, final String... names) {
92+
final List<String> containerNames = compose.listChildContainers().stream()
93+
.flatMap(container -> Stream.of(container.getNames()))
94+
.collect(Collectors.toList());
95+
96+
assertEquals("number of running services of docker-compose is the same as length of listOfServices",
97+
names.length, containerNames.size());
98+
99+
for (final String expectedName : names) {
100+
final long matches = containerNames.stream()
101+
.filter(foundName -> foundName.endsWith(expectedName))
102+
.count();
103+
104+
assertEquals("container with name starting '" + expectedName + "' should be running", 1L, matches);
105+
}
106+
}
107+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
version: '2.4'
2+
services:
3+
redis:
4+
image: redis
5+
other:
6+
image: alpine:3.5
7+
command: sleep 10000
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
version: '2.4'
2+
services:
3+
redis:
4+
image: redis
5+
scale: 3 # legacy mechanism to specify scale

0 commit comments

Comments
 (0)