Skip to content

Commit

Permalink
Adds ability to use local Docker compose binary instead of container.
Browse files Browse the repository at this point in the history
  • Loading branch information
outofcoffee committed Sep 24, 2016
1 parent 30183eb commit e1d9070
Showing 1 changed file with 131 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import com.github.dockerjava.api.model.Container;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.Uninterruptibles;
import org.apache.commons.lang.SystemUtils;
import org.junit.runner.Description;
Expand All @@ -17,9 +19,10 @@
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.containers.startupcheck.IndefiniteWaitOneShotStartupCheckStrategy;
import org.testcontainers.utility.Base58;
import org.testcontainers.utility.PathUtils;
import org.testcontainers.utility.ResourceReaper;
import org.testcontainers.utility.*;
import org.zeroturnaround.exec.InvalidExitValueException;
import org.zeroturnaround.exec.ProcessExecutor;
import org.zeroturnaround.exec.stream.slf4j.Slf4jStream;

import java.io.File;
import java.util.HashMap;
Expand All @@ -46,6 +49,7 @@ public class DockerComposeContainer<SELF extends DockerComposeContainer<SELF>> e
private Set<String> spawnedContainerIds;
private Map<String, Integer> scalingPreferences = new HashMap<>();
private DockerClient dockerClient;
private boolean localCompose;

/**
* Properties that should be passed through to all Compose and ambassador containers (not
Expand Down Expand Up @@ -73,11 +77,12 @@ public DockerComposeContainer(File composeFile, String identifier) {
this.dockerClient = DockerClientFactory.instance().client();
}

@Override @VisibleForTesting
@Override
@VisibleForTesting
public void starting(Description description) {
final Profiler profiler = new Profiler("Docker compose container rule");
final Profiler profiler = new Profiler("Docker Compose container rule");
profiler.setLogger(logger());
profiler.start("Docker compose container startup");
profiler.start("Docker Compose container startup");

pullImages();
applyScaling(); // scale before up, so that all scaled instances are available first for linking
Expand All @@ -89,7 +94,7 @@ public void starting(Description description) {

private void pullImages() {
getDockerCompose("pull")
.start();
.start();
}


Expand All @@ -100,7 +105,13 @@ private void createServices() {
}

private DockerCompose getDockerCompose(String cmd) {
return new DockerCompose(composeFile, identifier)
final DockerCompose dockerCompose;
if (localCompose) {
dockerCompose = new LocalDockerCompose(composeFile, identifier);
} else {
dockerCompose = new ContainerisedDockerCompose(composeFile, identifier);
}
return dockerCompose
.withCommand(cmd)
.withEnv(env);
}
Expand Down Expand Up @@ -135,7 +146,7 @@ private void registerContainersForShutdown() {
// Compose can define their own networks as well; ensure these are cleaned up
dockerClient.listNetworksCmd().exec().forEach(network -> {
if (network.getName().contains(identifier)) {
ResourceReaper.instance().registerNetworkForCleanup(network.getName());
ResourceReaper.instance().registerNetworkForCleanup(network.getName());
}
});

Expand Down Expand Up @@ -181,7 +192,8 @@ private Logger logger() {
return LoggerFactory.getLogger(DockerComposeContainer.class);
}

@Override @VisibleForTesting
@Override
@VisibleForTesting
public void finished(Description description) {

// shut down all the ambassador containers
Expand All @@ -201,7 +213,7 @@ public void finished(Description description) {

public SELF withExposedService(String serviceName, int servicePort) {

if (! serviceName.matches(".*_[0-9]+")) {
if (!serviceName.matches(".*_[0-9]+")) {
serviceName += "_1"; // implicit first instance of this service
}

Expand Down Expand Up @@ -276,16 +288,40 @@ public SELF withEnv(Map<String, String> env) {
return self();
}

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

private SELF self() {
return (SELF) this;
}
}

class DockerCompose extends GenericContainer<DockerCompose> {
public DockerCompose(File composeFile, String identifier) {
interface DockerCompose {
String ENV_PROJECT_NAME = "COMPOSE_PROJECT_NAME";
String ENV_COMPOSE_FILE = "COMPOSE_FILE";

DockerCompose withCommand(String cmd);

DockerCompose withEnv(Map<String, String> env);

void start();
}

/**
* Use Docker Compose container.
*/
class ContainerisedDockerCompose extends GenericContainer<ContainerisedDockerCompose> implements DockerCompose {
public ContainerisedDockerCompose(File composeFile, String identifier) {

super("docker/compose:1.8.0");
addEnv("COMPOSE_PROJECT_NAME", identifier);
addEnv(ENV_PROJECT_NAME, identifier);
// Map the docker compose file into the container
String pwd = composeFile.getAbsoluteFile().getParentFile().getAbsolutePath();
String containerPwd = pwd;
Expand All @@ -294,7 +330,7 @@ public DockerCompose(File composeFile, String identifier) {
containerPwd = PathUtils.createMinGWPath(containerPwd).substring(1);
}

addEnv("COMPOSE_FILE", containerPwd + "/" + composeFile.getAbsoluteFile().getName());
addEnv(ENV_COMPOSE_FILE, new File(containerPwd, composeFile.getName()).getAbsolutePath());
addFileSystemBind(pwd, containerPwd, READ_ONLY);
// Ensure that compose can access docker. Since the container is assumed to be running on the same machine
// as the docker daemon, just mapping the docker control socket is OK.
Expand All @@ -313,11 +349,89 @@ public void start() {
this.followOutput(new Slf4jLogConsumer(logger()));

// wait for the compose container to stop, which should only happen after it has spawned all the service containers
logger().info("Docker compose container is running for command: {}", Joiner.on(" ").join(this.getCommandParts()));
logger().info("Docker Compose container is running for command: {}", Joiner.on(" ").join(this.getCommandParts()));
while (this.isRunning()) {
logger().trace("Compose container is still running");
Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS);
}
logger().info("Docker compose has finished running");
logger().info("Docker Compose has finished running");
}
}

/**
* Use local Docker Compose binary, if present.
*/
class LocalDockerCompose implements DockerCompose {
/**
* Executable name for Docker Compose.
*/
private static final String COMPOSE_EXECUTABLE = "docker-compose";

private final File composeFile;
private final String identifier;
private String cmd = "";
private Map<String, String> env = new HashMap<>();

public LocalDockerCompose(File composeFile, String identifier) {
this.composeFile = composeFile;
this.identifier = identifier;
}

@Override
public DockerCompose withCommand(String cmd) {
this.cmd = cmd;
return this;
}

@Override
public DockerCompose withEnv(Map<String, String> env) {
this.env = env;
return this;
}

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

final Map<String, String> environment = Maps.newHashMap(env);
environment.put(ENV_PROJECT_NAME, identifier);

final File pwd = composeFile.getAbsoluteFile().getParentFile().getAbsoluteFile();
environment.put(ENV_COMPOSE_FILE, new File(pwd, composeFile.getAbsoluteFile().getName()).getAbsolutePath());

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

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

try {
new ProcessExecutor().command(command)
.redirectOutput(Slf4jStream.of(logger()).asInfo())
.redirectError(Slf4jStream.of(logger()).asError())
.environment(environment)
.directory(pwd)
.exitValueNormal()
.executeNoTimeout();

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

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

} catch (Exception e) {
throw new ContainerLaunchException("Error running local Docker Compose command: " + cmd, e);
}
}

/**
* @return a logger
*/
private Logger logger() {
return DockerLoggerFactory.getLogger(COMPOSE_EXECUTABLE);
}
}

0 comments on commit e1d9070

Please sign in to comment.