diff --git a/circle.yml b/circle.yml index cb9242532..8473231e0 100644 --- a/circle.yml +++ b/circle.yml @@ -13,7 +13,7 @@ dependencies: - sudo pip install docker-compose override: - ./gradlew resolveConfigurations - + test: pre: - ./gradlew findbugsMain findbugsTest checkstyleMain checkstyleTest javadoc --info diff --git a/src/main/java/com/palantir/docker/compose/DockerComposeRule.java b/src/main/java/com/palantir/docker/compose/DockerComposeRule.java index 386523f7a..3989878b4 100644 --- a/src/main/java/com/palantir/docker/compose/DockerComposeRule.java +++ b/src/main/java/com/palantir/docker/compose/DockerComposeRule.java @@ -17,11 +17,14 @@ import com.palantir.docker.compose.connection.waiting.ClusterHealthCheck; import com.palantir.docker.compose.connection.waiting.ClusterWait; import com.palantir.docker.compose.connection.waiting.HealthCheck; +import com.palantir.docker.compose.execution.ConflictingContainerRemovingDockerCompose; import com.palantir.docker.compose.execution.DefaultDockerCompose; +import com.palantir.docker.compose.execution.Docker; import com.palantir.docker.compose.execution.DockerCompose; import com.palantir.docker.compose.execution.DockerComposeExecArgument; import com.palantir.docker.compose.execution.DockerComposeExecOption; import com.palantir.docker.compose.execution.DockerComposeExecutable; +import com.palantir.docker.compose.execution.DockerExecutable; import com.palantir.docker.compose.execution.RetryingDockerCompose; import com.palantir.docker.compose.logging.DoNothingLogCollector; import com.palantir.docker.compose.logging.FileLogCollector; @@ -61,7 +64,7 @@ public ProjectName projectName() { } @Value.Default - public DockerComposeExecutable executable() { + public DockerComposeExecutable dockerComposeExecutable() { return DockerComposeExecutable.builder() .dockerComposeFiles(files()) .dockerConfiguration(machine()) @@ -69,9 +72,21 @@ public DockerComposeExecutable executable() { .build(); } + @Value.Default + public DockerExecutable dockerExecutable() { + return DockerExecutable.builder() + .dockerConfiguration(machine()) + .build(); + } + + @Value.Default + public Docker docker() { + return new Docker(dockerExecutable()); + } + @Value.Default public DockerCompose dockerCompose() { - DockerCompose dockerCompose = new DefaultDockerCompose(executable(), machine()); + DockerCompose dockerCompose = new DefaultDockerCompose(dockerComposeExecutable(), machine()); return new RetryingDockerCompose(retryAttempts(), dockerCompose); } @@ -93,6 +108,11 @@ protected boolean skipShutdown() { return false; } + @Value.Default + protected boolean removeConflictingContainersOnStartup() { + return true; + } + @Value.Default protected LogCollector logCollector() { return new DoNothingLogCollector(); @@ -102,7 +122,12 @@ protected LogCollector logCollector() { public void before() throws IOException, InterruptedException { log.debug("Starting docker-compose cluster"); dockerCompose().build(); - dockerCompose().up(); + + DockerCompose upDockerCompose = dockerCompose(); + if (removeConflictingContainersOnStartup()) { + upDockerCompose = new ConflictingContainerRemovingDockerCompose(upDockerCompose, docker()); + } + upDockerCompose.up(); log.debug("Starting log collection"); @@ -187,4 +212,5 @@ public ImmutableDockerComposeRule.Builder waitingForHostNetworkedPort(int port, return addClusterWait(new ClusterWait(clusterHealthCheck, timeout)); } } + } diff --git a/src/main/java/com/palantir/docker/compose/DockerCompositionBuilder.java b/src/main/java/com/palantir/docker/compose/DockerCompositionBuilder.java index 3728a034f..c1d60ecc5 100644 --- a/src/main/java/com/palantir/docker/compose/DockerCompositionBuilder.java +++ b/src/main/java/com/palantir/docker/compose/DockerCompositionBuilder.java @@ -88,6 +88,12 @@ public DockerCompositionBuilder saveLogsTo(String path) { return this; } + + public DockerCompositionBuilder removeConflictingContainersOnStartup(boolean removeConflictingContainersOnStartup) { + builder.removeConflictingContainersOnStartup(removeConflictingContainersOnStartup); + return this; + } + public DockerCompositionBuilder retryAttempts(int retryAttempts) { builder.retryAttempts(retryAttempts); return this; diff --git a/src/main/java/com/palantir/docker/compose/execution/SynchronousDockerComposeExecutable.java b/src/main/java/com/palantir/docker/compose/execution/Command.java similarity index 63% rename from src/main/java/com/palantir/docker/compose/execution/SynchronousDockerComposeExecutable.java rename to src/main/java/com/palantir/docker/compose/execution/Command.java index ebcc0f3cb..becf63a95 100644 --- a/src/main/java/com/palantir/docker/compose/execution/SynchronousDockerComposeExecutable.java +++ b/src/main/java/com/palantir/docker/compose/execution/Command.java @@ -24,26 +24,49 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.util.Arrays; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; -public class SynchronousDockerComposeExecutable { +public class Command { public static final int HOURS_TO_WAIT_FOR_STD_OUT_TO_CLOSE = 12; public static final int MINUTES_TO_WAIT_AFTER_STD_OUT_CLOSES = 1; - private final DockerComposeExecutable dockerComposeExecutable; + private final Executable executable; private final Consumer logConsumer; - public SynchronousDockerComposeExecutable(DockerComposeExecutable dockerComposeExecutable, - Consumer logConsumer) { - this.dockerComposeExecutable = dockerComposeExecutable; + public Command(Executable executable, Consumer logConsumer) { + this.executable = executable; this.logConsumer = logConsumer; } - public ProcessResult run(String... commands) throws IOException, InterruptedException { - Process process = dockerComposeExecutable.execute(commands); + public String execute(ErrorHandler errorHandler, String... commands) throws IOException, InterruptedException { + ProcessResult result = run(commands); + + if (result.exitCode() != 0) { + errorHandler.handle(result.exitCode(), result.output(), executable.commandName(), commands); + } + + return result.output(); + } + + public static ErrorHandler throwingOnError() { + return (exitCode, output, commandName, commands) -> { + String message = + constructNonZeroExitErrorMessage(exitCode, commandName, commands) + "\nThe output was:\n" + output; + throw new DockerExecutionException(message); + }; + } + + private static String constructNonZeroExitErrorMessage(int exitCode, String commandName, String... commands) { + return "'" + commandName + " " + Arrays.stream(commands).collect(joining(" ")) + "' returned exit code " + + exitCode; + } + + private ProcessResult run(String... commands) throws IOException, InterruptedException { + Process process = executable.execute(commands); Future outputProcessing = newSingleThreadExecutor() .submit(() -> processOutputFrom(process)); @@ -57,8 +80,8 @@ public ProcessResult run(String... commands) throws IOException, InterruptedExce private String processOutputFrom(Process process) { return asReader(process.getInputStream()).lines() - .peek(logConsumer) - .collect(joining(System.lineSeparator())); + .peek(logConsumer) + .collect(joining(System.lineSeparator())); } private String waitForResultFrom(Future outputProcessing) { diff --git a/src/main/java/com/palantir/docker/compose/execution/ConflictingContainerRemovingDockerCompose.java b/src/main/java/com/palantir/docker/compose/execution/ConflictingContainerRemovingDockerCompose.java new file mode 100644 index 000000000..29ddf60ed --- /dev/null +++ b/src/main/java/com/palantir/docker/compose/execution/ConflictingContainerRemovingDockerCompose.java @@ -0,0 +1,90 @@ +/* + * Copyright 2016 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.palantir.docker.compose.execution; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableSet; +import java.io.IOException; +import java.util.Collection; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ConflictingContainerRemovingDockerCompose extends DelegatingDockerCompose { + private static final Logger log = LoggerFactory.getLogger(ConflictingContainerRemovingDockerCompose.class); + private static final Pattern NAME_CONFLICT_PATTERN = Pattern.compile("The name \"([^\"]*)\" is already in use"); + + private final Docker docker; + private final int retryAttempts; + + public ConflictingContainerRemovingDockerCompose(DockerCompose dockerCompose, Docker docker) { + this(dockerCompose, docker, 1); + } + + public ConflictingContainerRemovingDockerCompose(DockerCompose dockerCompose, Docker docker, int retryAttempts) { + super(dockerCompose); + + Preconditions.checkArgument(retryAttempts >= 1, "retryAttempts must be at least 1, was " + retryAttempts); + this.docker = docker; + this.retryAttempts = retryAttempts; + } + + @Override + public void up() throws IOException, InterruptedException { + for (int currRetryAttempt = 0; currRetryAttempt <= retryAttempts; currRetryAttempt++) { + try { + getDockerCompose().up(); + return; + } catch (DockerExecutionException e) { + Set conflictingContainerNames = getConflictingContainerNames(e.getMessage()); + if (conflictingContainerNames.isEmpty()) { + // failed due to reason other than conflicting containers, so re-throw + throw e; + } + + log.debug("docker-compose up failed due to container name conflicts (container names: {}). " + + "Removing containers and attempting docker-compose up again (attempt {}).", + conflictingContainerNames, currRetryAttempt + 1); + removeContainers(conflictingContainerNames); + } + } + + throw new DockerExecutionException("docker-compose up failed"); + } + + private void removeContainers(Collection containerNames) throws IOException, InterruptedException { + try { + docker.rm(containerNames); + } catch (DockerExecutionException e) { + // there are cases such as in CircleCI where 'docker rm' returns a non-0 exit code and "fails", + // but container is still effectively removed as far as conflict resolution is concerned. Because + // of this, be permissive and do not fail task even if 'rm' fails. + log.debug("docker rm failed, but continuing execution", e); + } + } + + private static Set getConflictingContainerNames(String output) { + ImmutableSet.Builder builder = ImmutableSet.builder(); + Matcher matcher = NAME_CONFLICT_PATTERN.matcher(output); + while (matcher.find()) { + builder.add(matcher.group(1)); + } + return builder.build(); + } + +} diff --git a/src/main/java/com/palantir/docker/compose/execution/DefaultDockerCompose.java b/src/main/java/com/palantir/docker/compose/execution/DefaultDockerCompose.java index 1f8090374..b96ddcdb4 100644 --- a/src/main/java/com/palantir/docker/compose/execution/DefaultDockerCompose.java +++ b/src/main/java/com/palantir/docker/compose/execution/DefaultDockerCompose.java @@ -16,7 +16,6 @@ package com.palantir.docker.compose.execution; import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static java.util.stream.Collectors.joining; import static org.apache.commons.lang3.Validate.validState; import static org.joda.time.Duration.standardMinutes; @@ -31,7 +30,6 @@ import com.palantir.docker.compose.connection.Ports; import java.io.IOException; import java.io.OutputStream; -import java.util.Arrays; import org.apache.commons.io.IOUtils; import org.joda.time.Duration; import org.slf4j.Logger; @@ -43,7 +41,7 @@ public class DefaultDockerCompose implements DockerCompose { private static final Duration COMMAND_TIMEOUT = standardMinutes(2); private static final Logger log = LoggerFactory.getLogger(DefaultDockerCompose.class); - private final SynchronousDockerComposeExecutable executable; + private final Command command; private final DockerMachine dockerMachine; private final DockerComposeExecutable rawExecutable; @@ -57,33 +55,33 @@ public DefaultDockerCompose(DockerComposeFiles dockerComposeFiles, DockerMachine public DefaultDockerCompose(DockerComposeExecutable rawExecutable, DockerMachine dockerMachine) { this.rawExecutable = rawExecutable; - this.executable = new SynchronousDockerComposeExecutable(rawExecutable, log::debug); + this.command = new Command(rawExecutable, log::debug); this.dockerMachine = dockerMachine; } @Override public void build() throws IOException, InterruptedException { - executeDockerComposeCommand(throwingOnError(), "build"); + command.execute(Command.throwingOnError(), "build"); } @Override public void up() throws IOException, InterruptedException { - executeDockerComposeCommand(throwingOnError(), "up", "-d"); + command.execute(Command.throwingOnError(), "up", "-d"); } @Override public void down() throws IOException, InterruptedException { - executeDockerComposeCommand(swallowingDownCommandDoesNotExist(), "down", "--volumes"); + command.execute(swallowingDownCommandDoesNotExist(), "down", "--volumes"); } @Override public void kill() throws IOException, InterruptedException { - executeDockerComposeCommand(throwingOnError(), "kill"); + command.execute(Command.throwingOnError(), "kill"); } @Override public void rm() throws IOException, InterruptedException { - executeDockerComposeCommand(throwingOnError(), "rm", "--force", "-v"); + command.execute(Command.throwingOnError(), "rm", "--force", "-v"); } @Override @@ -91,7 +89,7 @@ public String exec(DockerComposeExecOption dockerComposeExecOption, String conta DockerComposeExecArgument dockerComposeExecArgument) throws IOException, InterruptedException { verifyDockerComposeVersionAtLeast(VERSION_1_7_0, "You need at least docker-compose 1.7 to run docker-compose exec"); String[] fullArgs = constructFullDockerComposeExecArguments(dockerComposeExecOption, containerName, dockerComposeExecArgument); - return executeDockerComposeCommand(throwingOnError(), fullArgs); + return command.execute(Command.throwingOnError(), fullArgs); } private void verifyDockerComposeVersionAtLeast(Version targetVersion, String message) throws IOException, InterruptedException { @@ -99,7 +97,7 @@ private void verifyDockerComposeVersionAtLeast(Version targetVersion, String mes } private Version version() throws IOException, InterruptedException { - String versionOutput = executeDockerComposeCommand(throwingOnError(), "-v"); + String versionOutput = command.execute(Command.throwingOnError(), "-v"); return DockerComposeVersion.parseFromDockerComposeVersion(versionOutput); } @@ -115,7 +113,7 @@ private String[] constructFullDockerComposeExecArguments(DockerComposeExecOption @Override public ContainerNames ps() throws IOException, InterruptedException { - String psOutput = executeDockerComposeCommand(throwingOnError(), "ps"); + String psOutput = command.execute(Command.throwingOnError(), "ps"); return ContainerNames.parseFromDockerComposePs(psOutput); } @@ -150,38 +148,15 @@ private Process followLogs(String container) throws IOException, InterruptedExce @Override public Ports ports(String service) throws IOException, InterruptedException { - String psOutput = executeDockerComposeCommand(throwingOnError(), "ps", service); + String psOutput = command.execute(Command.throwingOnError(), "ps", service); validState(!Strings.isNullOrEmpty(psOutput), "No container with name '" + service + "' found"); return Ports.parseFromDockerComposePs(psOutput, dockerMachine.getIp()); } - private ErrorHandler throwingOnError() { - return (exitCode, output, commands) -> { - String message = constructNonZeroExitErrorMessage(exitCode, commands) + "\nThe output was:\n" + output; - throw new DockerComposeExecutionException(message); - }; - } - - private String executeDockerComposeCommand(ErrorHandler errorHandler, String... commands) - throws IOException, InterruptedException { - ProcessResult result = executable.run(commands); - - if (result.exitCode() != 0) { - errorHandler.handle(result.exitCode(), result.output(), commands); - } - - return result.output(); - } - - - private String constructNonZeroExitErrorMessage(int exitCode, String... commands) { - return "'docker-compose " + Arrays.stream(commands).collect(joining(" ")) + "' returned exit code " + exitCode; - } - private ErrorHandler swallowingDownCommandDoesNotExist() { - return (exitCode, output, commands) -> { + return (exitCode, output, commandName, commands) -> { if (downCommandWasPresent(output)) { - throwingOnError().handle(exitCode, output, commands); + Command.throwingOnError().handle(exitCode, output, commandName, commands); } log.warn("It looks like `docker-compose down` didn't work."); diff --git a/src/main/java/com/palantir/docker/compose/execution/DelegatingDockerCompose.java b/src/main/java/com/palantir/docker/compose/execution/DelegatingDockerCompose.java new file mode 100644 index 000000000..3ea17adb4 --- /dev/null +++ b/src/main/java/com/palantir/docker/compose/execution/DelegatingDockerCompose.java @@ -0,0 +1,86 @@ +/* + * Copyright 2016 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.palantir.docker.compose.execution; + +import com.palantir.docker.compose.connection.Container; +import com.palantir.docker.compose.connection.ContainerNames; +import com.palantir.docker.compose.connection.Ports; +import java.io.IOException; +import java.io.OutputStream; + +abstract class DelegatingDockerCompose implements DockerCompose { + private final DockerCompose dockerCompose; + + protected DelegatingDockerCompose(DockerCompose dockerCompose) { + this.dockerCompose = dockerCompose; + } + + @Override + public void build() throws IOException, InterruptedException { + dockerCompose.build(); + } + + @Override + public void up() throws IOException, InterruptedException { + dockerCompose.up(); + } + + @Override + public void down() throws IOException, InterruptedException { + dockerCompose.down(); + } + + @Override + public void kill() throws IOException, InterruptedException { + dockerCompose.kill(); + } + + @Override + public void rm() throws IOException, InterruptedException { + dockerCompose.rm(); + } + + @Override + public String exec(DockerComposeExecOption dockerComposeExecOption, String containerName, + DockerComposeExecArgument dockerComposeExecArgument) throws IOException, InterruptedException { + return dockerCompose.exec(dockerComposeExecOption, containerName, dockerComposeExecArgument); + } + + @Override + public ContainerNames ps() throws IOException, InterruptedException { + return dockerCompose.ps(); + } + + @Override + public Container container(String containerName) { + return dockerCompose.container(containerName); + } + + @Override + public boolean writeLogs(String container, OutputStream output) throws IOException { + return dockerCompose.writeLogs(container, output); + } + + @Override + public Ports ports(String service) throws IOException, InterruptedException { + return dockerCompose.ports(service); + } + + protected final DockerCompose getDockerCompose() { + return dockerCompose; + } + +} diff --git a/src/main/java/com/palantir/docker/compose/execution/Docker.java b/src/main/java/com/palantir/docker/compose/execution/Docker.java new file mode 100644 index 000000000..3cb6b067d --- /dev/null +++ b/src/main/java/com/palantir/docker/compose/execution/Docker.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.palantir.docker.compose.execution; + +import com.google.common.collect.ObjectArrays; +import java.io.IOException; +import java.util.Collection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Docker { + + private static final Logger log = LoggerFactory.getLogger(Docker.class); + + private final Command command; + + public Docker(DockerExecutable rawExecutable) { + this.command = new Command(rawExecutable, log::debug); + } + + public void rm(Collection containerNames) throws IOException, InterruptedException { + rm(containerNames.toArray(new String[containerNames.size()])); + } + + public void rm(String... containerNames) throws IOException, InterruptedException { + command.execute(Command.throwingOnError(), + ObjectArrays.concat(new String[] {"rm", "-f"}, containerNames, String.class)); + } + +} diff --git a/src/main/java/com/palantir/docker/compose/execution/DockerComposeLocations.java b/src/main/java/com/palantir/docker/compose/execution/DockerCommandLocations.java similarity index 89% rename from src/main/java/com/palantir/docker/compose/execution/DockerComposeLocations.java rename to src/main/java/com/palantir/docker/compose/execution/DockerCommandLocations.java index 200f98e9a..a1e0c0a06 100644 --- a/src/main/java/com/palantir/docker/compose/execution/DockerComposeLocations.java +++ b/src/main/java/com/palantir/docker/compose/execution/DockerCommandLocations.java @@ -22,13 +22,13 @@ import java.util.Optional; import java.util.function.Predicate; -public class DockerComposeLocations { +public class DockerCommandLocations { private static final Predicate IS_NOT_NULL = path -> path != null; private static final Predicate FILE_EXISTS = path -> new File(path).exists(); private final List possiblePaths; - public DockerComposeLocations(String... possiblePaths) { + public DockerCommandLocations(String... possiblePaths) { this.possiblePaths = asList(possiblePaths); } @@ -42,6 +42,6 @@ public Optional preferredLocation() { @Override public String toString() { - return "DockerComposeLocations{possiblePaths=" + possiblePaths + "}"; + return "DockerCommandLocations{possiblePaths=" + possiblePaths + "}"; } } diff --git a/src/main/java/com/palantir/docker/compose/execution/DockerComposeExecutable.java b/src/main/java/com/palantir/docker/compose/execution/DockerComposeExecutable.java index 3770ee86d..7a580b130 100644 --- a/src/main/java/com/palantir/docker/compose/execution/DockerComposeExecutable.java +++ b/src/main/java/com/palantir/docker/compose/execution/DockerComposeExecutable.java @@ -25,12 +25,13 @@ import org.slf4j.LoggerFactory; @Value.Immutable -public abstract class DockerComposeExecutable { +public abstract class DockerComposeExecutable implements Executable { private static final Logger log = LoggerFactory.getLogger(DockerComposeExecutable.class); - private static final DockerComposeLocations DOCKER_COMPOSE_LOCATIONS = new DockerComposeLocations( + private static final DockerCommandLocations DOCKER_COMPOSE_LOCATIONS = new DockerCommandLocations( System.getenv("DOCKER_COMPOSE_LOCATION"), - "/usr/local/bin/docker-compose" + "/usr/local/bin/docker-compose", + "/usr/bin/docker-compose" ); @Value.Parameter protected abstract DockerComposeFiles dockerComposeFiles(); @@ -40,6 +41,11 @@ public abstract class DockerComposeExecutable { return ProjectName.random(); } + @Override + public final String commandName() { + return "docker-compose"; + } + @Value.Derived protected String dockerComposePath() { String pathToUse = DOCKER_COMPOSE_LOCATIONS.preferredLocation() @@ -51,6 +57,7 @@ protected String dockerComposePath() { return pathToUse; } + @Override public Process execute(String... commands) throws IOException { List args = ImmutableList.builder() .add(dockerComposePath()) diff --git a/src/main/java/com/palantir/docker/compose/execution/DockerExecutable.java b/src/main/java/com/palantir/docker/compose/execution/DockerExecutable.java new file mode 100644 index 000000000..1ff5cf01e --- /dev/null +++ b/src/main/java/com/palantir/docker/compose/execution/DockerExecutable.java @@ -0,0 +1,69 @@ +/* + * Copyright 2016 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.palantir.docker.compose.execution; + +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.util.List; +import org.immutables.value.Value; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Value.Immutable +public abstract class DockerExecutable implements Executable { + private static final Logger log = LoggerFactory.getLogger(DockerExecutable.class); + + private static final DockerCommandLocations DOCKER_LOCATIONS = new DockerCommandLocations( + System.getenv("DOCKER_LOCATION"), + "/usr/local/bin/docker", + "/usr/bin/docker" + ); + + @Value.Parameter protected abstract DockerConfiguration dockerConfiguration(); + + @Override + public final String commandName() { + return "docker"; + } + + @Value.Derived + protected String dockerPath() { + String pathToUse = DOCKER_LOCATIONS.preferredLocation() + .orElseThrow(() -> new IllegalStateException( + "Could not find docker, looked in: " + DOCKER_LOCATIONS)); + + log.debug("Using docker found at " + pathToUse); + + return pathToUse; + } + + @Override + public Process execute(String... commands) throws IOException { + List args = ImmutableList.builder() + .add(dockerPath()) + .add(commands) + .build(); + + return dockerConfiguration().configuredDockerComposeProcess() + .command(args) + .redirectErrorStream(true) + .start(); + } + + public static ImmutableDockerExecutable.Builder builder() { + return ImmutableDockerExecutable.builder(); + } +} diff --git a/src/main/java/com/palantir/docker/compose/execution/DockerComposeExecutionException.java b/src/main/java/com/palantir/docker/compose/execution/DockerExecutionException.java similarity index 79% rename from src/main/java/com/palantir/docker/compose/execution/DockerComposeExecutionException.java rename to src/main/java/com/palantir/docker/compose/execution/DockerExecutionException.java index 5bef1f98d..bab99b7c9 100644 --- a/src/main/java/com/palantir/docker/compose/execution/DockerComposeExecutionException.java +++ b/src/main/java/com/palantir/docker/compose/execution/DockerExecutionException.java @@ -15,11 +15,11 @@ */ package com.palantir.docker.compose.execution; -public class DockerComposeExecutionException extends RuntimeException { - public DockerComposeExecutionException() { +public class DockerExecutionException extends RuntimeException { + public DockerExecutionException() { } - public DockerComposeExecutionException(String message) { + public DockerExecutionException(String message) { super(message); } } diff --git a/src/main/java/com/palantir/docker/compose/execution/ErrorHandler.java b/src/main/java/com/palantir/docker/compose/execution/ErrorHandler.java index 6a13a9834..185f09b53 100644 --- a/src/main/java/com/palantir/docker/compose/execution/ErrorHandler.java +++ b/src/main/java/com/palantir/docker/compose/execution/ErrorHandler.java @@ -17,5 +17,5 @@ @FunctionalInterface public interface ErrorHandler { - void handle(int exitCode, String output, String... commands); + void handle(int exitCode, String output, String commandName, String... commands); } diff --git a/src/main/java/com/palantir/docker/compose/execution/Executable.java b/src/main/java/com/palantir/docker/compose/execution/Executable.java new file mode 100644 index 000000000..880eae83b --- /dev/null +++ b/src/main/java/com/palantir/docker/compose/execution/Executable.java @@ -0,0 +1,26 @@ +/* + * Copyright 2016 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.palantir.docker.compose.execution; + +import java.io.IOException; + +public interface Executable { + + String commandName(); + + Process execute(String... commands) throws IOException; + +} diff --git a/src/main/java/com/palantir/docker/compose/execution/Retryer.java b/src/main/java/com/palantir/docker/compose/execution/Retryer.java index 1914d3f07..f3f2dc734 100644 --- a/src/main/java/com/palantir/docker/compose/execution/Retryer.java +++ b/src/main/java/com/palantir/docker/compose/execution/Retryer.java @@ -22,7 +22,7 @@ public class Retryer { private static final Logger log = LoggerFactory.getLogger(Retryer.class); - public interface RetryableDockerComposeOperation { + public interface RetryableDockerOperation { T call() throws IOException, InterruptedException; } @@ -32,12 +32,12 @@ public Retryer(int retryAttempts) { this.retryAttempts = retryAttempts; } - public T runWithRetries(RetryableDockerComposeOperation operation) throws IOException, InterruptedException { - DockerComposeExecutionException lastExecutionException = null; + public T runWithRetries(RetryableDockerOperation operation) throws IOException, InterruptedException { + DockerExecutionException lastExecutionException = null; for (int i = 0; i <= retryAttempts; i++) { try { return operation.call(); - } catch (DockerComposeExecutionException e) { + } catch (DockerExecutionException e) { lastExecutionException = e; log.warn("Caught exception: " + e.getMessage() + ". Retrying."); } diff --git a/src/main/java/com/palantir/docker/compose/execution/RetryingDockerCompose.java b/src/main/java/com/palantir/docker/compose/execution/RetryingDockerCompose.java index 9e373daf7..77ced9094 100644 --- a/src/main/java/com/palantir/docker/compose/execution/RetryingDockerCompose.java +++ b/src/main/java/com/palantir/docker/compose/execution/RetryingDockerCompose.java @@ -15,76 +15,31 @@ */ package com.palantir.docker.compose.execution; -import com.palantir.docker.compose.connection.Container; import com.palantir.docker.compose.connection.ContainerNames; -import com.palantir.docker.compose.connection.Ports; import java.io.IOException; -import java.io.OutputStream; -public class RetryingDockerCompose implements DockerCompose { +public class RetryingDockerCompose extends DelegatingDockerCompose { private final Retryer retryer; - private final DockerCompose dockerCompose; public RetryingDockerCompose(int retryAttempts, DockerCompose dockerCompose) { this(new Retryer(retryAttempts), dockerCompose); } public RetryingDockerCompose(Retryer retryer, DockerCompose dockerCompose) { + super(dockerCompose); this.retryer = retryer; - this.dockerCompose = dockerCompose; - } - - @Override - public void build() throws IOException, InterruptedException { - dockerCompose.build(); } @Override public void up() throws IOException, InterruptedException { retryer.runWithRetries(() -> { - dockerCompose.up(); + super.up(); return null; }); } - @Override - public void down() throws IOException, InterruptedException { - dockerCompose.down(); - } - - @Override - public void kill() throws IOException, InterruptedException { - dockerCompose.kill(); - } - - @Override - public void rm() throws IOException, InterruptedException { - dockerCompose.rm(); - } - - @Override - public String exec(DockerComposeExecOption dockerComposeExecOption, String containerName, - DockerComposeExecArgument dockerComposeExecArgument) throws IOException, InterruptedException { - return dockerCompose.exec(dockerComposeExecOption, containerName, dockerComposeExecArgument); - } - @Override public ContainerNames ps() throws IOException, InterruptedException { - return retryer.runWithRetries(dockerCompose::ps); - } - - @Override - public Container container(String containerName) { - return dockerCompose.container(containerName); - } - - @Override - public boolean writeLogs(String container, OutputStream output) throws IOException { - return dockerCompose.writeLogs(container, output); - } - - @Override - public Ports ports(String service) throws IOException, InterruptedException { - return dockerCompose.ports(service); + return retryer.runWithRetries(super::ps); } } diff --git a/src/test/java/com/palantir/docker/compose/DockerComposeRuleRestartContainersIntegrationTest.java b/src/test/java/com/palantir/docker/compose/DockerComposeRuleRestartContainersIntegrationTest.java new file mode 100644 index 000000000..0329dc822 --- /dev/null +++ b/src/test/java/com/palantir/docker/compose/DockerComposeRuleRestartContainersIntegrationTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2016 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.palantir.docker.compose; + +import com.palantir.docker.compose.execution.DockerExecutionException; +import java.io.IOException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class DockerComposeRuleRestartContainersIntegrationTest { + + private static final String DOCKER_COMPOSE_YAML_PATH = "src/test/resources/named-containers-docker-compose.yaml"; + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void test_docker_compose_rule_fails_with_existing_containers() throws IOException, InterruptedException { + DockerComposition composition = DockerComposition.of(DOCKER_COMPOSE_YAML_PATH).build(); + composition.before(); + composition = DockerComposition.of(DOCKER_COMPOSE_YAML_PATH) + .removeConflictingContainersOnStartup(false) + .build(); + + exception.expect(DockerExecutionException.class); + exception.expectMessage("'docker-compose up -d' returned exit code"); + composition.before(); + } + + @Test + public void test_docker_compose_rule_removes_existing_containers() throws IOException, InterruptedException { + DockerComposition composition = DockerComposition.of(DOCKER_COMPOSE_YAML_PATH).build(); + composition.before(); + + composition = DockerComposition.of(DOCKER_COMPOSE_YAML_PATH).build(); + composition.before(); + composition.after(); + } + +} diff --git a/src/test/java/com/palantir/docker/compose/DockerComposeRuleShould.java b/src/test/java/com/palantir/docker/compose/DockerComposeRuleShould.java index de0d1bb3a..a1cf1e1db 100644 --- a/src/test/java/com/palantir/docker/compose/DockerComposeRuleShould.java +++ b/src/test/java/com/palantir/docker/compose/DockerComposeRuleShould.java @@ -24,6 +24,7 @@ import static org.joda.time.Duration.millis; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -31,6 +32,7 @@ import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.palantir.docker.compose.configuration.DockerComposeFiles; import com.palantir.docker.compose.configuration.MockDockerEnvironment; import com.palantir.docker.compose.connection.Container; @@ -39,7 +41,9 @@ import com.palantir.docker.compose.connection.DockerPort; import com.palantir.docker.compose.connection.waiting.HealthCheck; import com.palantir.docker.compose.connection.waiting.SuccessOrFailure; +import com.palantir.docker.compose.execution.Docker; import com.palantir.docker.compose.execution.DockerCompose; +import com.palantir.docker.compose.execution.DockerExecutionException; import com.palantir.docker.compose.logging.LogCollector; import java.io.File; import java.io.IOException; @@ -72,6 +76,7 @@ public class DockerComposeRuleShould { private HealthCheck> healthCheck; private final DockerCompose dockerCompose = mock(DockerCompose.class); + private final Docker mockDocker = mock(Docker.class); private final MockDockerEnvironment env = new MockDockerEnvironment(dockerCompose); private DockerComposeFiles mockFiles = mock(DockerComposeFiles.class); private DockerMachine machine = mock(DockerMachine.class); @@ -217,6 +222,40 @@ public void not_shut_down_when_skipShutdown_is_true() throws InterruptedExceptio verify(logCollector, times(1)).stopCollecting(); } + @Test + public void before_fails_when_docker_up_throws_exception() throws IOException, InterruptedException { + doThrow(new DockerExecutionException("")).when(dockerCompose).up(); + rule = defaultBuilder().build(); + exception.expect(DockerExecutionException.class); + rule.before(); + } + + @Test + public void before_retries_when_docker_up_reports_conflicting_containers() throws IOException, InterruptedException { + String conflictingContainer = "conflictingContainer"; + doThrow(new DockerExecutionException("The name \"" + conflictingContainer + "\" is already in use")) + .doNothing() + .when(dockerCompose).up(); + rule = defaultBuilder().docker(mockDocker).build(); + rule.before(); + + verify(dockerCompose, times(2)).up(); + verify(mockDocker).rm(ImmutableSet.of(conflictingContainer)); + } + + @Test + public void when_remove_conflicting_containers_on_startup_is_set_to_false_before_does_not_retry_on_conflicts() + throws IOException, InterruptedException { + String conflictingContainer = "conflictingContainer"; + doThrow(new DockerExecutionException("The name \"" + conflictingContainer + "\" is already in use")) + .when(dockerCompose).up(); + rule = defaultBuilder().docker(mockDocker).removeConflictingContainersOnStartup(false).build(); + + exception.expect(DockerExecutionException.class); + exception.expectMessage("The name \"conflictingContainer\" is already in use"); + rule.before(); + } + public Container withComposeExecutableReturningContainerFor(String containerName) { final Container container = new Container(containerName, dockerCompose); when(dockerCompose.container(containerName)).thenReturn(container); diff --git a/src/test/java/com/palantir/docker/compose/execution/SynchronousDockerComposeExecutableShould.java b/src/test/java/com/palantir/docker/compose/execution/CommandShould.java similarity index 61% rename from src/test/java/com/palantir/docker/compose/execution/SynchronousDockerComposeExecutableShould.java rename to src/test/java/com/palantir/docker/compose/execution/CommandShould.java index 8f49db14f..e06e2a71c 100644 --- a/src/test/java/com/palantir/docker/compose/execution/SynchronousDockerComposeExecutableShould.java +++ b/src/test/java/com/palantir/docker/compose/execution/CommandShould.java @@ -20,6 +20,8 @@ import static org.hamcrest.Matchers.contains; import static org.hamcrest.core.Is.is; import static org.mockito.Matchers.anyVararg; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import java.io.IOException; @@ -33,45 +35,63 @@ import org.mockito.runners.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) -public class SynchronousDockerComposeExecutableShould { +public class CommandShould { @Mock private Process executedProcess; @Mock private DockerComposeExecutable dockerComposeExecutable; - private SynchronousDockerComposeExecutable dockerCompose; + @Mock private ErrorHandler errorHandler; + private Command dockerComposeCommand; private final List consumedLogLines = new ArrayList<>(); private final Consumer logConsumer = s -> consumedLogLines.add(s); @Before public void setup() throws IOException { when(dockerComposeExecutable.execute(anyVararg())).thenReturn(executedProcess); - dockerCompose = new SynchronousDockerComposeExecutable(dockerComposeExecutable, logConsumer); + dockerComposeCommand = new Command(dockerComposeExecutable, logConsumer); givenTheUnderlyingProcessHasOutput(""); givenTheUnderlyingProcessTerminatesWithAnExitCodeOf(0); } @Test public void - respond_with_the_exit_code_of_the_executed_process() throws IOException, InterruptedException { + invoke_error_handler_when_exit_code_of_the_executed_process_is_non_0() throws IOException, InterruptedException { int expectedExitCode = 1; - givenTheUnderlyingProcessTerminatesWithAnExitCodeOf(expectedExitCode); + dockerComposeCommand.execute(errorHandler, "rm", "-f"); - assertThat(dockerCompose.run("rm", "-f").exitCode(), is(expectedExitCode)); + verify(errorHandler).handle(expectedExitCode, "", "docker-compose", "rm", "-f"); } @Test public void - respond_with_the_output_of_the_executed_process() throws IOException, InterruptedException { - String expectedOutput = "some output"; + not_invoke_error_handler_when_exit_code_of_the_executed_process_is_0() throws IOException, InterruptedException { + dockerComposeCommand.execute(errorHandler, "rm", "-f"); + + verifyZeroInteractions(errorHandler); + } + @Test public void + return_output_when_exit_code_of_the_executed_process_is_non_0() throws IOException, InterruptedException { + String expectedOutput = "test output"; + givenTheUnderlyingProcessTerminatesWithAnExitCodeOf(1); + givenTheUnderlyingProcessHasOutput(expectedOutput); + String commandOutput = dockerComposeCommand.execute(errorHandler, "rm", "-f"); + + assertThat(commandOutput, is(expectedOutput)); + } + + @Test public void + return_output_when_exit_code_of_the_executed_process_is_0() throws IOException, InterruptedException { + String expectedOutput = "test output"; givenTheUnderlyingProcessHasOutput(expectedOutput); + String commandOutput = dockerComposeCommand.execute(errorHandler, "rm", "-f"); - assertThat(dockerCompose.run("rm", "-f").output(), is(expectedOutput)); + assertThat(commandOutput, is(expectedOutput)); } @Test public void give_the_output_to_the_specified_consumer_as_it_is_available() throws IOException, InterruptedException { givenTheUnderlyingProcessHasOutput("line 1\nline 2"); - dockerCompose.run("rm", "-f"); + dockerComposeCommand.execute(errorHandler, "rm", "-f"); assertThat(consumedLogLines, contains("line 1", "line 2")); } diff --git a/src/test/java/com/palantir/docker/compose/execution/ConflictingContainerRemovingDockerComposeShould.java b/src/test/java/com/palantir/docker/compose/execution/ConflictingContainerRemovingDockerComposeShould.java new file mode 100644 index 000000000..4655b8d91 --- /dev/null +++ b/src/test/java/com/palantir/docker/compose/execution/ConflictingContainerRemovingDockerComposeShould.java @@ -0,0 +1,131 @@ +/* + * Copyright 2016 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.palantir.docker.compose.execution; + +import static org.mockito.Matchers.anySet; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +import com.google.common.collect.ImmutableSet; +import java.io.IOException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class ConflictingContainerRemovingDockerComposeShould { + private final DockerCompose dockerCompose = mock(DockerCompose.class); + private final Docker docker = mock(Docker.class); + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void require_retry_attempts_to_be_at_least_1() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("retryAttempts must be at least 1, was 0"); + new ConflictingContainerRemovingDockerCompose(dockerCompose, docker, 0); + } + + @Test + public void call_up_only_once_if_successful() throws IOException, InterruptedException { + ConflictingContainerRemovingDockerCompose conflictingContainerRemovingDockerCompose = + new ConflictingContainerRemovingDockerCompose(dockerCompose, docker); + conflictingContainerRemovingDockerCompose.up(); + + verify(dockerCompose, times(1)).up(); + verifyZeroInteractions(docker); + } + + @Test + public void call_rm_and_retry_up_if_conflicting_containers_exist() throws IOException, InterruptedException { + String conflictingContainer = "conflictingContainer"; + doThrow(new DockerExecutionException("The name \"" + conflictingContainer + "\" is already in use")) + .doNothing() + .when(dockerCompose).up(); + + ConflictingContainerRemovingDockerCompose conflictingContainerRemovingDockerCompose = + new ConflictingContainerRemovingDockerCompose(dockerCompose, docker); + conflictingContainerRemovingDockerCompose.up(); + + verify(dockerCompose, times(2)).up(); + verify(docker).rm(ImmutableSet.of(conflictingContainer)); + } + + @Test + public void retry_specified_number_of_times() throws IOException, InterruptedException { + String conflictingContainer = "conflictingContainer"; + DockerExecutionException dockerException = new DockerExecutionException( + "The name \"" + conflictingContainer + "\" is already in use"); + doThrow(dockerException) + .doThrow(dockerException) + .doNothing() + .when(dockerCompose).up(); + + ConflictingContainerRemovingDockerCompose conflictingContainerRemovingDockerCompose = + new ConflictingContainerRemovingDockerCompose(dockerCompose, docker, 3); + conflictingContainerRemovingDockerCompose.up(); + + verify(dockerCompose, times(3)).up(); + verify(docker, times(2)).rm(ImmutableSet.of(conflictingContainer)); + } + + @Test + public void ignore_docker_execution_exceptions_in_rm() throws IOException, InterruptedException { + String conflictingContainer = "conflictingContainer"; + doThrow(new DockerExecutionException("The name \"" + conflictingContainer + "\" is already in use")) + .doNothing() + .when(dockerCompose).up(); + doThrow(DockerExecutionException.class).when(docker).rm(anySet()); + + ConflictingContainerRemovingDockerCompose conflictingContainerRemovingDockerCompose = + new ConflictingContainerRemovingDockerCompose(dockerCompose, docker); + conflictingContainerRemovingDockerCompose.up(); + + verify(dockerCompose, times(2)).up(); + verify(docker).rm(ImmutableSet.of(conflictingContainer)); + } + + @Test + public void fail_on_non_docker_execution_exceptions_in_rm() throws IOException, InterruptedException { + String conflictingContainer = "conflictingContainer"; + doThrow(new DockerExecutionException("The name \"" + conflictingContainer + "\" is already in use")) + .doNothing() + .when(dockerCompose).up(); + doThrow(RuntimeException.class).when(docker).rm(anySet()); + + exception.expect(RuntimeException.class); + ConflictingContainerRemovingDockerCompose conflictingContainerRemovingDockerCompose = + new ConflictingContainerRemovingDockerCompose(dockerCompose, docker); + conflictingContainerRemovingDockerCompose.up(); + } + + @Test + public void throw_exception_if_retry_attempts_exceeded() throws IOException, InterruptedException { + String conflictingContainer = "conflictingContainer"; + doThrow(new DockerExecutionException("The name \"" + conflictingContainer + "\" is already in use")) + .when(dockerCompose).up(); + + exception.expect(DockerExecutionException.class); + exception.expectMessage("docker-compose up failed"); + ConflictingContainerRemovingDockerCompose conflictingContainerRemovingDockerCompose = + new ConflictingContainerRemovingDockerCompose(dockerCompose, docker); + conflictingContainerRemovingDockerCompose.up(); + } + +} diff --git a/src/test/java/com/palantir/docker/compose/execution/DockerComposeLocationsShould.java b/src/test/java/com/palantir/docker/compose/execution/DockerCommandLocationsShould.java similarity index 77% rename from src/test/java/com/palantir/docker/compose/execution/DockerComposeLocationsShould.java rename to src/test/java/com/palantir/docker/compose/execution/DockerCommandLocationsShould.java index 5dbbc0072..222e6cf53 100644 --- a/src/test/java/com/palantir/docker/compose/execution/DockerComposeLocationsShould.java +++ b/src/test/java/com/palantir/docker/compose/execution/DockerCommandLocationsShould.java @@ -25,7 +25,7 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; -public class DockerComposeLocationsShould { +public class DockerCommandLocationsShould { private static final String badLocation = "file/that/does/not/exist"; private static final String otherBadLocation = "another/file/that/does/not/exist"; @@ -39,32 +39,32 @@ public void setup() throws IOException { } @Test public void - provide_the_first_docker_compose_location_if_it_exists() throws IOException { - DockerComposeLocations dockerComposeLocations = new DockerComposeLocations( + provide_the_first_docker_command_location_if_it_exists() throws IOException { + DockerCommandLocations dockerCommandLocations = new DockerCommandLocations( badLocation, goodLocation, otherBadLocation); - assertThat(dockerComposeLocations.preferredLocation().get(), + assertThat(dockerCommandLocations.preferredLocation().get(), is(goodLocation)); } @Test public void skip_paths_from_environment_variables_that_are_unset() { - DockerComposeLocations dockerComposeLocations = new DockerComposeLocations( + DockerCommandLocations dockerCommandLocations = new DockerCommandLocations( System.getenv("AN_UNSET_DOCKER_COMPOSE_PATH"), goodLocation); - assertThat(dockerComposeLocations.preferredLocation().get(), + assertThat(dockerCommandLocations.preferredLocation().get(), is(goodLocation)); } @Test public void have_no_preferred_path_when_all_possible_paths_are_all_invalid() { - DockerComposeLocations dockerComposeLocations = new DockerComposeLocations( + DockerCommandLocations dockerCommandLocations = new DockerCommandLocations( badLocation); - assertThat(dockerComposeLocations.preferredLocation(), + assertThat(dockerCommandLocations.preferredLocation(), is(empty())); } } diff --git a/src/test/java/com/palantir/docker/compose/execution/DockerComposeShould.java b/src/test/java/com/palantir/docker/compose/execution/DockerComposeShould.java index fa6420dc4..dd9503d61 100644 --- a/src/test/java/com/palantir/docker/compose/execution/DockerComposeShould.java +++ b/src/test/java/com/palantir/docker/compose/execution/DockerComposeShould.java @@ -103,7 +103,7 @@ public void call_docker_compose_with_the_follow_flag_when_the_version_is_at_leas @Test public void throw_exception_when_kill_exits_with_a_non_zero_exit_code() throws IOException, InterruptedException { when(executedProcess.exitValue()).thenReturn(1); - exception.expect(DockerComposeExecutionException.class); + exception.expect(DockerExecutionException.class); exception.expectMessage("'docker-compose kill' returned exit code 1"); compose.kill(); } @@ -122,7 +122,7 @@ public void throw_exception_when_down_fails_for_a_reason_other_than_the_command_ when(executedProcess.exitValue()).thenReturn(1); when(executedProcess.getInputStream()).thenReturn(toInputStream("")); - exception.expect(DockerComposeExecutionException.class); + exception.expect(DockerExecutionException.class); compose.down(); } diff --git a/src/test/java/com/palantir/docker/compose/execution/DockerShould.java b/src/test/java/com/palantir/docker/compose/execution/DockerShould.java new file mode 100644 index 000000000..57e77d99f --- /dev/null +++ b/src/test/java/com/palantir/docker/compose/execution/DockerShould.java @@ -0,0 +1,49 @@ +/* + * Copyright 2016 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.palantir.docker.compose.execution; + +import static org.apache.commons.io.IOUtils.toInputStream; +import static org.mockito.Matchers.anyVararg; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import org.junit.Before; +import org.junit.Test; + +public class DockerShould { + + private final DockerExecutable executor = mock(DockerExecutable.class); + private final Docker docker = new Docker(executor); + + private final Process executedProcess = mock(Process.class); + + @Before + public void setup() throws IOException, InterruptedException { + when(executor.execute(anyVararg())).thenReturn(executedProcess); + when(executedProcess.getInputStream()).thenReturn(toInputStream("0.0.0.0:7000->7000/tcp")); + when(executedProcess.exitValue()).thenReturn(0); + } + + @Test + public void call_docker_rm_with_force_flag_on_rm() throws IOException, InterruptedException { + docker.rm("testContainer"); + + verify(executor).execute("rm", "-f", "testContainer"); + } + +} diff --git a/src/test/java/com/palantir/docker/compose/execution/RetryerShould.java b/src/test/java/com/palantir/docker/compose/execution/RetryerShould.java index 608a3571c..4bc1a6c1e 100644 --- a/src/test/java/com/palantir/docker/compose/execution/RetryerShould.java +++ b/src/test/java/com/palantir/docker/compose/execution/RetryerShould.java @@ -27,7 +27,7 @@ import org.junit.Test; public class RetryerShould { - private final Retryer.RetryableDockerComposeOperation operation = mock(Retryer.RetryableDockerComposeOperation.class); + private final Retryer.RetryableDockerOperation operation = mock(Retryer.RetryableDockerOperation.class); private final Retryer retryer = new Retryer(1); @Test @@ -41,7 +41,7 @@ public void not_retry_if_the_operation_was_successful_and_return_result() throws @Test public void retry_the_operation_if_it_failed_once_and_return_the_result_of_the_next_successful_call() throws Exception { when(operation.call()).thenAnswer(MockitoMultiAnswer.of( - firstInvocation -> { throw new DockerComposeExecutionException(); }, + firstInvocation -> { throw new DockerExecutionException(); }, secondInvocation -> "hola" )); @@ -51,17 +51,17 @@ public void retry_the_operation_if_it_failed_once_and_return_the_result_of_the_n @Test public void throw_the_last_exception_when_the_operation_fails_more_times_than_the_number_of_specified_retry_attempts() throws Exception { - DockerComposeExecutionException finalException = new DockerComposeExecutionException(); + DockerExecutionException finalException = new DockerExecutionException(); when(operation.call()).thenAnswer(MockitoMultiAnswer.of( - firstInvocation -> { throw new DockerComposeExecutionException(); }, + firstInvocation -> { throw new DockerExecutionException(); }, secondInvocation -> { throw finalException; } )); try { retryer.runWithRetries(operation); fail("Should have caught exception"); - } catch (DockerComposeExecutionException actualException) { + } catch (DockerExecutionException actualException) { assertThat(actualException, is(finalException)); } diff --git a/src/test/java/com/palantir/docker/compose/execution/RetryingDockerComposeShould.java b/src/test/java/com/palantir/docker/compose/execution/RetryingDockerComposeShould.java index ca95b4e6c..3ef6f2dda 100644 --- a/src/test/java/com/palantir/docker/compose/execution/RetryingDockerComposeShould.java +++ b/src/test/java/com/palantir/docker/compose/execution/RetryingDockerComposeShould.java @@ -44,8 +44,8 @@ public void before() throws IOException, InterruptedException { } private void retryerJustCallsOperation() throws IOException, InterruptedException { - when(retryer.runWithRetries(any(Retryer.RetryableDockerComposeOperation.class))).thenAnswer(invocation -> { - Retryer.RetryableDockerComposeOperation operation = (Retryer.RetryableDockerComposeOperation) invocation.getArguments()[0]; + when(retryer.runWithRetries(any(Retryer.RetryableDockerOperation.class))).thenAnswer(invocation -> { + Retryer.RetryableDockerOperation operation = (Retryer.RetryableDockerOperation) invocation.getArguments()[0]; return operation.call(); }); } @@ -71,11 +71,11 @@ public void call_ps_on_the_underlying_docker_compose_and_returns_the_same_value( } private void verifyRetryerWasUsed() throws IOException, InterruptedException { - verify(retryer).runWithRetries(any(Retryer.RetryableDockerComposeOperation.class)); + verify(retryer).runWithRetries(any(Retryer.RetryableDockerOperation.class)); } private void verifyRetryerWasNotUsed() throws IOException, InterruptedException { - verify(retryer, times(0)).runWithRetries(any(Retryer.RetryableDockerComposeOperation.class)); + verify(retryer, times(0)).runWithRetries(any(Retryer.RetryableDockerOperation.class)); } @Test diff --git a/src/test/resources/named-containers-docker-compose.yaml b/src/test/resources/named-containers-docker-compose.yaml new file mode 100644 index 000000000..1e5b8233e --- /dev/null +++ b/src/test/resources/named-containers-docker-compose.yaml @@ -0,0 +1,7 @@ +test-1: + container_name: test-1.container.name + image: kiasaki/alpine-postgres + +test-2: + container_name: test-2.container.name + image: kiasaki/alpine-postgres