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 all 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"area/docker-compose":
- core/src/main/java/org/testcontainers/containers/ComposeContainer.java
- core/src/main/java/org/testcontainers/containers/ComposeDelegate.java
- core/src/main/java/org/testcontainers/containers/DockerComposeContainer.java
- core/src/main/java/org/testcontainers/containers/DockerComposeFiles.java
"github_actions":
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,384 @@
package org.testcontainers.containers;

import com.github.dockerjava.api.model.Container;
import com.google.common.annotations.VisibleForTesting;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.SystemUtils;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.containers.wait.strategy.WaitStrategy;
import org.testcontainers.lifecycle.Startable;
import org.testcontainers.utility.Base58;
import org.testcontainers.utility.DockerImageName;

import java.io.File;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;

/**
* Testcontainers implementation for Docker Compose V2. <br>
* It uses either Compose V2 contained within the Docker binary, or a containerised version of Compose V2.
*/
@Slf4j
public class ComposeContainer extends FailureDetectingExternalResource implements Startable {

private final Map<String, Integer> scalingPreferences = new HashMap<>();

private boolean localCompose;

private boolean pull = true;

private boolean build = false;

private Set<String> options = new HashSet<>();

private boolean tailChildContainers;

private static final Object MUTEX = new Object();

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

/**
* Properties that should be passed through to all Compose and ambassador containers (not
* necessarily to containers that are spawned by Compose itself)
*/
private Map<String, String> env = new HashMap<>();

private RemoveImages removeImages;

private boolean removeVolumes = true;

public static final String COMPOSE_EXECUTABLE = SystemUtils.IS_OS_WINDOWS ? "docker.exe" : "docker";

private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("docker:24.0.2");

private final ComposeDelegate composeDelegate;

private String project;

public ComposeContainer(File... composeFiles) {
this(Arrays.asList(composeFiles));
}

public ComposeContainer(List<File> composeFiles) {
this(Base58.randomString(6).toLowerCase(), composeFiles);
}

public ComposeContainer(String identifier, File... composeFiles) {
this(identifier, Arrays.asList(composeFiles));
}

public ComposeContainer(String identifier, List<File> composeFiles) {
this.composeDelegate =
new ComposeDelegate(
ComposeDelegate.ComposeVersion.V2,
composeFiles,
identifier,
COMPOSE_EXECUTABLE,
DEFAULT_IMAGE_NAME
);
this.project = this.composeDelegate.getProject();
}

@Override
@Deprecated
public Statement apply(Statement base, Description description) {
return super.apply(base, description);
}

@Override
@Deprecated
public void starting(Description description) {
start();
}

@Override
@Deprecated
protected void succeeded(Description description) {}

@Override
@Deprecated
protected void failed(Throwable e, Description description) {}

@Override
@Deprecated
public void finished(Description description) {
stop();
}

@Override
public void start() {
synchronized (MUTEX) {
this.composeDelegate.registerContainersForShutdown();
if (pull) {
try {
this.composeDelegate.pullImages();
} catch (ContainerLaunchException e) {
log.warn("Exception while pulling images, using local images if available", e);
}
}
this.composeDelegate.createServices(
this.localCompose,
this.build,
this.options,
this.services,
this.scalingPreferences,
this.env
);
this.composeDelegate.startAmbassadorContainer();
this.composeDelegate.waitUntilServiceStarted(this.tailChildContainers);
}
}

@VisibleForTesting
List<Container> listChildContainers() {
return this.composeDelegate.listChildContainers();
}

public ComposeContainer withServices(@NonNull String... services) {
this.services = Arrays.asList(services);
return this;
}

@Override
public void stop() {
synchronized (MUTEX) {
try {
this.composeDelegate.getAmbassadorContainer().stop();

// Kill the services using docker
String cmd = "compose down";
if (removeVolumes) {
cmd += " -v";
}
if (removeImages != null) {
cmd += " --rmi " + removeImages.dockerRemoveImagesType();
}
this.composeDelegate.runWithCompose(this.localCompose, cmd);
} finally {
this.project = this.composeDelegate.randomProjectId();
}
}
}

public ComposeContainer withExposedService(String serviceName, int servicePort) {
this.composeDelegate.withExposedService(serviceName, servicePort, Wait.defaultWaitStrategy());
return this;
}

public ComposeContainer withExposedService(String serviceName, int instance, int servicePort) {
return withExposedService(serviceName + "-" + instance, servicePort);
}

public ComposeContainer withExposedService(
String serviceName,
int instance,
int servicePort,
WaitStrategy waitStrategy
) {
this.composeDelegate.withExposedService(serviceName + "-" + instance, servicePort, waitStrategy);
return this;
}

public ComposeContainer withExposedService(
String serviceName,
int servicePort,
@NonNull WaitStrategy waitStrategy
) {
this.composeDelegate.withExposedService(serviceName, servicePort, waitStrategy);
return this;
}

/**
* Specify the {@link WaitStrategy} to use to determine if the container is ready.
*
* @param serviceName the name of the service to wait for
* @param waitStrategy the WaitStrategy to use
* @return this
* @see org.testcontainers.containers.wait.strategy.Wait#defaultWaitStrategy()
*/
public ComposeContainer waitingFor(String serviceName, @NonNull WaitStrategy waitStrategy) {
String serviceInstanceName = this.composeDelegate.getServiceInstanceName(serviceName);
this.composeDelegate.addWaitStrategy(serviceInstanceName, waitStrategy);
return this;
}

/**
* Get the host (e.g. IP address or hostname) that an exposed service can be found at, from the host machine
* (i.e. should be the machine that's running this Java process).
* <p>
* The service must have been declared using ComposeContainer#withExposedService.
*
* @param serviceName the name of the service as set in the docker-compose.yml file.
* @param servicePort the port exposed by the service container.
* @return a host IP address or hostname that can be used for accessing the service container.
*/
public String getServiceHost(String serviceName, Integer servicePort) {
return this.composeDelegate.getServiceHost();
}

/**
* Get the port that an exposed service can be found at, from the host machine
* (i.e. should be the machine that's running this Java process).
* <p>
* The service must have been declared using ComposeContainer#withExposedService.
*
* @param serviceName the name of the service as set in the docker-compose.yml file.
* @param servicePort the port exposed by the service container.
* @return a port that can be used for accessing the service container.
*/
public Integer getServicePort(String serviceName, Integer servicePort) {
return this.composeDelegate.getServicePort(serviceName, servicePort);
}

public ComposeContainer withScaledService(String serviceBaseName, int numInstances) {
scalingPreferences.put(serviceBaseName, numInstances);
return this;
}

public ComposeContainer withEnv(String key, String value) {
env.put(key, value);
return this;
}

public ComposeContainer withEnv(Map<String, String> env) {
env.forEach(this.env::put);
return this;
}

/**
* Use a local Docker Compose binary instead of a container.
*
* @return this instance, for chaining
*/
public ComposeContainer withLocalCompose(boolean localCompose) {
this.localCompose = localCompose;
return this;
}

/**
* Whether to pull images first.
*
* @return this instance, for chaining
*/
public ComposeContainer withPull(boolean pull) {
this.pull = pull;
return this;
}

/**
* Whether to tail child container logs.
*
* @return this instance, for chaining
*/
public ComposeContainer withTailChildContainers(boolean tailChildContainers) {
this.tailChildContainers = tailChildContainers;
return this;
}

/**
* Attach an output consumer at container startup, enabling stdout and stderr to be followed, waited on, etc.
* <p>
* More than one consumer may be registered.
*
* @param serviceName the name of the service as set in the docker-compose.yml file
* @param consumer consumer that output frames should be sent to
* @return this instance, for chaining
*/
public ComposeContainer withLogConsumer(String serviceName, Consumer<OutputFrame> consumer) {
this.composeDelegate.withLogConsumer(serviceName, consumer);
return this;
}

/**
* Whether to always build images before starting containers.
*
* @return this instance, for chaining
*/
public ComposeContainer withBuild(boolean build) {
this.build = build;
return this;
}

/**
* Adds options to the docker command, e.g. docker --compatibility.
*
* @return this instance, for chaining
*/
public ComposeContainer withOptions(String... options) {
this.options = new HashSet<>(Arrays.asList(options));
return this;
}

/**
* Remove images after containers shutdown.
*
* @return this instance, for chaining
*/
public ComposeContainer withRemoveImages(ComposeContainer.RemoveImages removeImages) {
this.removeImages = removeImages;
return this;
}

/**
* Remove volumes after containers shut down.
*
* @param removeVolumes whether volumes are to be removed.
* @return this instance, for chaining.
*/
public ComposeContainer withRemoveVolumes(boolean removeVolumes) {
this.removeVolumes = removeVolumes;
return this;
}

/**
* Set the maximum startup timeout all the waits set are bounded to.
*
* @return this instance. for chaining
*/
public ComposeContainer withStartupTimeout(Duration startupTimeout) {
this.composeDelegate.setStartupTimeout(startupTimeout);
return this;
}

public Optional<ContainerState> getContainerByServiceName(String serviceName) {
return this.composeDelegate.getContainerByServiceName(serviceName);
}

private void followLogs(String containerId, Consumer<OutputFrame> consumer) {
this.followLogs(containerId, consumer);
}

public enum RemoveImages {
/**
* Remove all images used by any service.
*/
ALL("all"),

/**
* Remove only images that don't have a custom tag set by the `image` field.
*/
LOCAL("local");

private final String dockerRemoveImagesType;

RemoveImages(final String dockerRemoveImagesType) {
this.dockerRemoveImagesType = dockerRemoveImagesType;
}

public String dockerRemoveImagesType() {
return dockerRemoveImagesType;
}
}
}