-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for compose v2 with ComposeContainer (#5608)
[Compose V2](https://www.docker.com/blog/announcing-compose-v2-general-availability/) offers arm images to perform `docker compose` commands. Co-authored-by: Kevin Wittek <kiview@users.noreply.github.com>
- Loading branch information
1 parent
d106d3f
commit 7112db5
Showing
28 changed files
with
2,136 additions
and
542 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
384 changes: 384 additions & 0 deletions
384
core/src/main/java/org/testcontainers/containers/ComposeContainer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
Oops, something went wrong.