Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for compose v2 with ComposeContainer #5608

Merged
merged 48 commits into from
Jun 21, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
8573b7e
Add support for compose v2
eddumelendez Jul 24, 2022
9f3983b
Add configuration property to enable compose v2 compatibility
eddumelendez Jul 28, 2022
2f4c569
Add docs
eddumelendez Jul 28, 2022
cb139d5
Fix tests
eddumelendez Jul 28, 2022
69dbf78
Fix assumption
eddumelendez Jul 28, 2022
7f9e995
Update docs/modules/docker_compose.md
eddumelendez Jul 29, 2022
48b15a9
Autodetect compose v2
eddumelendez Jul 29, 2022
2cdfb5c
Add testcontainers prefix to config property
eddumelendez Jul 29, 2022
deb5f2b
Merge branch 'master' into composev2
eddumelendez Jul 29, 2022
afbc537
Fix test
eddumelendez Jul 29, 2022
01f28f8
Fix test
eddumelendez Jul 30, 2022
e8e07c5
Fix comments
eddumelendez Aug 2, 2022
6b670d4
Set composev2 enabled by default and add compatibility mode
eddumelendez Aug 13, 2022
c8b49b4
Merge branch 'master' into composev2
eddumelendez Aug 13, 2022
1fd85c5
Fix test
eddumelendez Aug 13, 2022
d65db21
Update missing test
eddumelendez Aug 13, 2022
1b4c884
Add ComposeContainer
eddumelendez Aug 16, 2022
f7d875f
Remove test
eddumelendez Aug 16, 2022
f1436d6
Remove configuration doc
eddumelendez Aug 16, 2022
e67ebc6
Merge branch 'master' into composev2
eddumelendez Aug 31, 2022
a252f29
Changes
eddumelendez Sep 1, 2022
b6c9ddd
Refactor DockerComposeContainer
eddumelendez Sep 26, 2022
3d2c056
Merge branch 'master' into composev2
eddumelendez Sep 26, 2022
153b9ef
Merge branch 'main' into composev2
eddumelendez Sep 30, 2022
8e37af6
Rename and introduce ComposeVersion enum
eddumelendez Sep 30, 2022
2d07a92
Update docs/modules/docker_compose.md
eddumelendez Sep 30, 2022
dc785f1
Updates
eddumelendez Sep 30, 2022
40e08db
Update ComposeContainer
eddumelendez Sep 30, 2022
9619dee
Fix compose v2 start commmand
eddumelendez Oct 3, 2022
b6f0829
Add options in specific position for compose v2
eddumelendez Oct 3, 2022
04e04cb
Merge branch 'main' into composev2
eddumelendez Oct 3, 2022
10919cc
Fix test
eddumelendez Oct 3, 2022
2214f8a
Merge branch 'main' into composev2
eddumelendez Apr 14, 2023
ca78119
Copy compose file to work with remote docker
eddumelendez Apr 14, 2023
7bbf135
Update docker images to run in arm
eddumelendez Apr 18, 2023
c829746
Use different redis versions
eddumelendez Apr 19, 2023
196d180
Merge branch 'main' into composev2
eddumelendez Apr 29, 2023
337e0c7
Drop link in favor of networks
eddumelendez May 2, 2023
601148e
Merge branch 'main' into composev2
eddumelendez May 10, 2023
0840644
Add test for identifier in uppercase
eddumelendez May 10, 2023
6a39b89
Revert "Drop link in favor of networks"
eddumelendez May 10, 2023
c2dcc88
Merge branch 'main' into composev2
eddumelendez May 16, 2023
39b37d7
Fix test
eddumelendez May 17, 2023
a078382
Update core/src/main/java/org/testcontainers/containers/ComposeContai…
eddumelendez Jun 20, 2023
4c7ca00
Merge branch 'main' into composev2
eddumelendez Jun 20, 2023
9317f81
Add note
eddumelendez Jun 20, 2023
3e21972
Update docker version
eddumelendez Jun 20, 2023
7e2f2da
Update labeler.yml
eddumelendez Jun 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.testcontainers.utility.MountableFile;
import org.testcontainers.utility.PathUtils;
import org.testcontainers.utility.ResourceReaper;
import org.testcontainers.utility.TestcontainersConfiguration;
import org.zeroturnaround.exec.InvalidExitValueException;
import org.zeroturnaround.exec.ProcessExecutor;
import org.zeroturnaround.exec.stream.slf4j.Slf4jStream;
Expand Down Expand Up @@ -119,6 +120,8 @@ public class DockerComposeContainer<SELF extends DockerComposeContainer<SELF>>

private RemoveImages removeImages;

private boolean composeV2 = false;

@Deprecated
public DockerComposeContainer(File composeFile, String identifier) {
this(identifier, composeFile);
Expand All @@ -137,6 +140,7 @@ public DockerComposeContainer(String identifier, File... composeFiles) {
}

public DockerComposeContainer(String identifier, List<File> composeFiles) {
this.composeV2 = TestcontainersConfiguration.getInstance().isComposeV2Enabled();
this.composeFiles = composeFiles;
this.dockerComposeFiles = new DockerComposeFiles(composeFiles);

Expand Down Expand Up @@ -238,7 +242,7 @@ private void createServices() {
.distinct()
.collect(Collectors.joining(" "));

String command = optionsAsString() + "up -d";
String command = optionsAsString() + getUpCommand();

if (build) {
command += " --build";
Expand Down Expand Up @@ -318,7 +322,7 @@ private void waitUntilServiceStarted(String serviceName, ComposeServiceWaitStrat
private String getServiceNameFromContainer(Container container) {
final String containerName = container.getLabels().get("com.docker.compose.service");
final String containerNumber = container.getLabels().get("com.docker.compose.container-number");
return String.format("%s_%s", containerName, containerNumber);
return String.format("%s%s%s", containerName, composeSeparator(), containerNumber);
}

private void runWithCompose(String cmd) {
Expand All @@ -329,7 +333,10 @@ private void runWithCompose(String cmd) {
if (localCompose) {
dockerCompose = new LocalDockerCompose(composeFiles, project);
} else {
dockerCompose = new ContainerisedDockerCompose(composeFiles, project);
DockerImageName composeImageName = composeV2
? ContainerisedDockerCompose.DEFAULT_IMAGE_NAME
: ContainerisedDockerCompose.DEFAULT_COMPOSE_IMAGE_NAME;
dockerCompose = new ContainerisedDockerCompose(composeImageName, composeFiles, project);
}

dockerCompose.withCommand(cmd).withEnv(env).invoke();
Expand Down Expand Up @@ -366,7 +373,7 @@ public void stop() {
ambassadorContainer.stop();

// Kill the services using docker-compose
String cmd = "down -v";
String cmd = getDownCommand();
if (removeImages != null) {
cmd += " --rmi " + removeImages.dockerRemoveImagesType();
}
Expand All @@ -382,7 +389,7 @@ public SELF withExposedService(String serviceName, int servicePort) {
}

public DockerComposeContainer withExposedService(String serviceName, int instance, int servicePort) {
return withExposedService(serviceName + "_" + instance, servicePort);
return withExposedService(serviceName + composeSeparator() + instance, servicePort);
}

public DockerComposeContainer withExposedService(
Expand All @@ -391,7 +398,7 @@ public DockerComposeContainer withExposedService(
int servicePort,
WaitStrategy waitStrategy
) {
return withExposedService(serviceName + "_" + instance, servicePort, waitStrategy);
return withExposedService(serviceName + composeSeparator() + instance, servicePort, waitStrategy);
}

public SELF withExposedService(String serviceName, int servicePort, @NonNull WaitStrategy waitStrategy) {
Expand All @@ -417,15 +424,19 @@ public SELF withExposedService(String serviceName, int servicePort, @NonNull Wai
.computeIfAbsent(serviceInstanceName, __ -> new ConcurrentHashMap<>())
.put(servicePort, ambassadorPort);
ambassadorContainer.withTarget(ambassadorPort, serviceInstanceName, servicePort);
ambassadorContainer.addLink(new FutureContainer(this.project + "_" + serviceInstanceName), serviceInstanceName);
ambassadorContainer.addLink(
new FutureContainer(this.project + composeSeparator() + serviceInstanceName),
serviceInstanceName
);
addWaitStrategy(serviceInstanceName, waitStrategy);
return self();
}

private String getServiceInstanceName(String serviceName) {
String serviceInstanceName = serviceName;
if (!serviceInstanceName.matches(".*_[0-9]+")) {
serviceInstanceName += "_1"; // implicit first instance of this service
String regex = String.format(".*%s[0-9]+", composeSeparator());
if (!serviceInstanceName.matches(regex)) {
serviceInstanceName += String.format("%s1", composeSeparator()); // implicit first instance of this service
}
return serviceInstanceName;
}
Expand Down Expand Up @@ -504,6 +515,18 @@ public Integer getServicePort(String serviceName, Integer servicePort) {
}
}

private String getUpCommand() {
return composeV2 ? "compose up -d" : "up -d";
}

private String getDownCommand() {
return composeV2 ? "compose down -v" : "down -v";
}

private String composeSeparator() {
return composeV2 ? "-" : "_";
}

public SELF withScaledService(String serviceBaseName, int numInstances) {
scalingPreferences.put(serviceBaseName, numInstances);

Expand Down Expand Up @@ -653,12 +676,14 @@ interface DockerCompose {
*/
class ContainerisedDockerCompose extends GenericContainer<ContainerisedDockerCompose> implements DockerCompose {

public static final char UNIX_PATH_SEPERATOR = ':';
public static final char UNIX_PATH_SEPARATOR = ':';
kiview marked this conversation as resolved.
Show resolved Hide resolved

public static final DockerImageName DEFAULT_COMPOSE_IMAGE_NAME = DockerImageName.parse("docker/compose:1.29.2");

public static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("docker/compose:1.29.2");
public static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("docker:20.10.17");

public ContainerisedDockerCompose(List<File> composeFiles, String identifier) {
super(DEFAULT_IMAGE_NAME);
public ContainerisedDockerCompose(DockerImageName dockerImageName, List<File> composeFiles, String identifier) {
super(dockerImageName);
addEnv(ENV_PROJECT_NAME, identifier);

// Map the docker compose file into the container
Expand All @@ -673,7 +698,7 @@ public ContainerisedDockerCompose(List<File> composeFiles, String identifier) {
.map(MountableFile::getFilesystemPath)
.map(this::convertToUnixFilesystemPath)
.collect(Collectors.toList());
final String composeFileEnvVariableValue = Joiner.on(UNIX_PATH_SEPERATOR).join(absoluteDockerComposeFiles); // we always need the UNIX path separator
final String composeFileEnvVariableValue = Joiner.on(UNIX_PATH_SEPARATOR).join(absoluteDockerComposeFiles); // we always need the UNIX path separator
logger().debug("Set env COMPOSE_FILE={}", composeFileEnvVariableValue);
addEnv(ENV_COMPOSE_FILE, composeFileEnvVariableValue);
addFileSystemBind(pwd, containerPwd, BindMode.READ_WRITE);
Expand Down Expand Up @@ -742,6 +767,8 @@ class LocalDockerCompose implements DockerCompose {
? "docker-compose.exe"
: "docker-compose";

private static final String DOCKER_EXECUTABLE = SystemUtils.IS_OS_WINDOWS ? "docker.exe" : "docker";

private final List<File> composeFiles;

private final String identifier;
Expand All @@ -750,6 +777,10 @@ class LocalDockerCompose implements DockerCompose {

private Map<String, String> env = new HashMap<>();

private static String executable = TestcontainersConfiguration.getInstance().isComposeV2Enabled()
? DOCKER_EXECUTABLE
: COMPOSE_EXECUTABLE;

public LocalDockerCompose(List<File> composeFiles, String identifier) {
this.composeFiles = composeFiles;
this.identifier = identifier;
Expand All @@ -769,16 +800,14 @@ public DockerCompose withEnv(Map<String, String> env) {

@VisibleForTesting
static boolean executableExists() {
return CommandLine.executableExists(COMPOSE_EXECUTABLE);
return CommandLine.executableExists(executable);
}

@Override
public void invoke() {
// bail out early
if (!executableExists()) {
throw new ContainerLaunchException(
"Local Docker Compose not found. Is " + COMPOSE_EXECUTABLE + " on the PATH?"
);
throw new ContainerLaunchException("Local Docker Compose not found. Is " + executable + " on the PATH?");
}

final Map<String, String> environment = Maps.newHashMap(env);
Expand Down Expand Up @@ -815,10 +844,7 @@ public void invoke() {

logger().info("Local Docker Compose is running command: {}", cmd);

final List<String> command = Splitter
.onPattern(" ")
.omitEmptyStrings()
.splitToList(COMPOSE_EXECUTABLE + " " + cmd);
final List<String> command = Splitter.onPattern(" ").omitEmptyStrings().splitToList(executable + " " + cmd);

try {
new ProcessExecutor()
Expand Down Expand Up @@ -847,6 +873,6 @@ public void invoke() {
* @return a logger
*/
private Logger logger() {
return DockerLoggerFactory.getLogger(COMPOSE_EXECUTABLE);
return DockerLoggerFactory.getLogger(executable);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ public boolean environmentSupportsReuse() {
return Boolean.parseBoolean(getEnvVarOrUserProperty("testcontainers.reuse.enable", "false"));
}

public boolean isComposeV2Enabled() {
return Boolean.parseBoolean(getEnvVarOrUserProperty("composev2.enable", "false"));
eddumelendez marked this conversation as resolved.
Show resolved Hide resolved
}

public String getDockerClientStrategyClassName() {
// getConfigurable won't apply the TESTCONTAINERS_ prefix when looking for env vars if DOCKER_ appears at the beginning.
// Because of this overlap, and the desire to not change this specific TESTCONTAINERS_DOCKER_CLIENT_STRATEGY setting,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package org.testcontainers.junit;

import com.github.dockerjava.api.model.Network;
import org.junit.After;
import org.junit.Assume;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.DockerComposeContainer;
import org.testcontainers.utility.TestEnvironment;
import org.testcontainers.utility.TestcontainersConfiguration;
import redis.clients.jedis.Jedis;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import static org.hamcrest.Matchers.is;
import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals;
import static org.rnorth.visibleassertions.VisibleAssertions.assertThat;

public class DockerComposeV2Test {
eddumelendez marked this conversation as resolved.
Show resolved Hide resolved

@Rule
public DockerComposeContainer environment = new DockerComposeContainer(
new File("src/test/resources/v2-compose-test.yml")
)
.withExposedService("redis-1", REDIS_PORT);

protected static final int REDIS_PORT = 6379;

private List<String> existingNetworks = new ArrayList<>();

@BeforeClass
public static void checkVersion() {
Assume.assumeTrue(TestEnvironment.dockerApiAtLeast("1.22"));
Assume.assumeTrue(TestcontainersConfiguration.getInstance().isComposeV2Enabled());
}

@Test
public void simpleTest() {
Jedis jedis = new Jedis(
this.environment.getServiceHost("redis-1", REDIS_PORT),
this.environment.getServicePort("redis-1", REDIS_PORT)
);

jedis.incr("test");
jedis.incr("test");
jedis.incr("test");

assertEquals("A redis instance defined in compose can be used in isolation", "3", jedis.get("test"));
}

@Test
public void secondTest() {
eddumelendez marked this conversation as resolved.
Show resolved Hide resolved
// used in manual checking for cleanup in between tests
Jedis jedis = new Jedis(
this.environment.getServiceHost("redis-1", REDIS_PORT),
this.environment.getServicePort("redis-1", REDIS_PORT)
);

jedis.incr("test");
jedis.incr("test");
jedis.incr("test");

assertEquals("Tests use fresh container instances", "3", jedis.get("test"));
// if these end up using the same container one of the test methods will fail.
// However, @Rule creates a separate DockerComposeContainer instance per test, so this just shouldn't happen
}

@Before
public void captureNetworks() {
existingNetworks.addAll(findAllNetworks());
}

@After
public void verifyNoNetworks() {
eddumelendez marked this conversation as resolved.
Show resolved Hide resolved
assertThat("The networks", findAllNetworks(), is(existingNetworks));
}

private List<String> findAllNetworks() {
return DockerClientFactory
.instance()
.client()
.listNetworksCmd()
.exec()
.stream()
.map(Network::getName)
.sorted()
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,31 @@ public void shouldTrimImageNames() {
);
}

@Test
public void shouldNotReadComposeV2FromClasspathProperties() {
assertFalse("compose v2 enabled by default", newConfig().isComposeV2Enabled());

classpathProperties.setProperty("composev2.enable", "true");
assertFalse("compose v2 are not affected by classpath properties", newConfig().isComposeV2Enabled());
}

@Test
public void shouldReadComposeV2FromUserProperties() {
assertFalse("compose v2 enabled by default", newConfig().isComposeV2Enabled());

userProperties.setProperty("composev2.enable", "true");
assertTrue("compose v2 disabled via user properties", newConfig().isComposeV2Enabled());
}

@Test
public void shouldReadComposeV2FromEnvironment() {
assertFalse("compose v2 enabled by default", newConfig().isComposeV2Enabled());

userProperties.remove("composev2.enable");
environment.put("TESTCONTAINERS_COMPOSEV2_ENABLE", "true");
assertTrue("compose v2 disabled via env var", newConfig().isComposeV2Enabled());
}

private TestcontainersConfiguration newConfig() {
return new TestcontainersConfiguration(userProperties, classpathProperties, environment);
}
Expand Down
6 changes: 6 additions & 0 deletions docs/features/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,9 @@ In addition, you can deactivate this behaviour by specifying:
```properties
dockerconfig.source=autoIgnoringUserProperties # 'auto' by default
```

## Enabling Compose V2 compatibility

> **composev2.enable = [true|false]**
eddumelendez marked this conversation as resolved.
Show resolved Hide resolved

By enabling it, [Compose V2](https://www.docker.com/blog/announcing-compose-v2-general-availability/) will be used.
13 changes: 13 additions & 0 deletions docs/modules/docker_compose.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,19 @@ public static DockerComposeContainer environment =
.waitingFor("db_1", Wait.forLogMessage("started", 1))
.withLocalCompose(true);
```

## Compose V2

[Compose V2 is GA](https://www.docker.com/blog/announcing-compose-v2-general-availability/) and it relies in `docker` command instead of `docker-compose`.
eddumelendez marked this conversation as resolved.
Show resolved Hide resolved

```java
public static DockerComposeContainer environment =
new DockerComposeContainer(new File("src/test/resources/compose-test.yml"))
.withComposeV2()
.withExposedService("redis-1", REDIS_PORT, Wait.forListeningPort())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need an explicit note, that users need to be aware of using - instead of _ for V2 services in their code.

.waitingFor("db-1", Wait.forLogMessage("started", 1));
```
eddumelendez marked this conversation as resolved.
Show resolved Hide resolved

## Using private repositories in Docker compose
When Docker Compose is used in container mode (not local), it's needs to be made aware of Docker settings for private repositories.
By default, those setting are located in `$HOME/.docker/config.json`.
Expand Down