From 1d44397629bdf2ebde66654911836d3a16ff07b0 Mon Sep 17 00:00:00 2001 From: Chip Killmar Date: Wed, 25 Apr 2018 13:38:53 -0500 Subject: [PATCH] Add feature to choose which services to start with docker-compose up --- .../execution/DefaultDockerCompose.java | 35 +++++- .../execution/DelegatingDockerCompose.java | 5 + .../compose/execution/DockerCompose.java | 1 + .../compose/logging/FileLogCollector.java | 7 ++ .../logging/FileLogCollectorShould.java | 27 +++++ .../docker/compose/DockerComposeRule.java | 11 +- ...oseRulePartialServicesIntegrationTest.java | 101 ++++++++++++++++++ 7 files changed, 183 insertions(+), 4 deletions(-) create mode 100644 docker-compose-rule-junit4/src/test/java/com/palantir/docker/compose/DockerComposeRulePartialServicesIntegrationTest.java diff --git a/docker-compose-rule-core/src/main/java/com/palantir/docker/compose/execution/DefaultDockerCompose.java b/docker-compose-rule-core/src/main/java/com/palantir/docker/compose/execution/DefaultDockerCompose.java index cec5eeaff..99c33f899 100644 --- a/docker-compose-rule-core/src/main/java/com/palantir/docker/compose/execution/DefaultDockerCompose.java +++ b/docker-compose-rule-core/src/main/java/com/palantir/docker/compose/execution/DefaultDockerCompose.java @@ -22,6 +22,7 @@ import com.github.zafarkhaja.semver.Version; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; import com.palantir.docker.compose.configuration.DockerComposeFiles; import com.palantir.docker.compose.configuration.ProjectName; import com.palantir.docker.compose.connection.Container; @@ -32,6 +33,7 @@ import java.io.IOException; import java.io.OutputStream; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Optional; import org.apache.commons.io.IOUtils; @@ -48,20 +50,33 @@ public class DefaultDockerCompose implements DockerCompose { private final Command command; private final DockerMachine dockerMachine; private final DockerComposeExecutable rawExecutable; - + private final ImmutableList servicesToStart; public DefaultDockerCompose(DockerComposeFiles dockerComposeFiles, DockerMachine dockerMachine, ProjectName projectName) { this(DockerComposeExecutable.builder() .dockerComposeFiles(dockerComposeFiles) .dockerConfiguration(dockerMachine) .projectName(projectName) - .build(), dockerMachine); + .build(), dockerMachine, Collections.emptyList()); + } + + public DefaultDockerCompose(DockerComposeFiles dockerComposeFiles, DockerMachine dockerMachine, ProjectName projectName, List servicesToStart) { + this(DockerComposeExecutable.builder() + .dockerComposeFiles(dockerComposeFiles) + .dockerConfiguration(dockerMachine) + .projectName(projectName) + .build(), dockerMachine, servicesToStart); } public DefaultDockerCompose(DockerComposeExecutable rawExecutable, DockerMachine dockerMachine) { + this(rawExecutable, dockerMachine, Collections.emptyList()); + } + + public DefaultDockerCompose(DockerComposeExecutable rawExecutable, DockerMachine dockerMachine, List servicesToStart) { this.rawExecutable = rawExecutable; this.command = new Command(rawExecutable, log::trace); this.dockerMachine = dockerMachine; + this.servicesToStart = ImmutableList.copyOf(servicesToStart); } @Override @@ -76,7 +91,9 @@ public void build() throws IOException, InterruptedException { @Override public void up() throws IOException, InterruptedException { - command.execute(Command.throwingOnError(), "up", "-d"); + List commands = Lists.newArrayList("up", "-d"); + commands.addAll(servicesToStart); + command.execute(Command.throwingOnError(), commands.toArray(new String[0])); } @Override @@ -188,6 +205,18 @@ public List services() throws IOException, InterruptedException { return Arrays.asList(servicesOutput.split("(\r|\n)+")); } + /** + * The names of services to start, via {@code docker-compose up -d SERVICE...}. If empty (the + * default), all services are started. This can be a subset of the services defined in + * the Docker Compose files. + * + * @return the names of services to start + */ + @Override + public List servicesToStart() { + return servicesToStart; + } + /** * Blocks until all logs collected from the container. * @return Whether the docker container terminated prior to log collection ending diff --git a/docker-compose-rule-core/src/main/java/com/palantir/docker/compose/execution/DelegatingDockerCompose.java b/docker-compose-rule-core/src/main/java/com/palantir/docker/compose/execution/DelegatingDockerCompose.java index e3fda2003..a5f37b0c9 100644 --- a/docker-compose-rule-core/src/main/java/com/palantir/docker/compose/execution/DelegatingDockerCompose.java +++ b/docker-compose-rule-core/src/main/java/com/palantir/docker/compose/execution/DelegatingDockerCompose.java @@ -112,6 +112,11 @@ public List services() throws IOException, InterruptedException { return dockerCompose.services(); } + @Override + public List servicesToStart() { + return dockerCompose.servicesToStart(); + } + @Override public boolean writeLogs(String container, OutputStream output) throws IOException { return dockerCompose.writeLogs(container, output); diff --git a/docker-compose-rule-core/src/main/java/com/palantir/docker/compose/execution/DockerCompose.java b/docker-compose-rule-core/src/main/java/com/palantir/docker/compose/execution/DockerCompose.java index 08cce8fe8..0d47e1562 100644 --- a/docker-compose-rule-core/src/main/java/com/palantir/docker/compose/execution/DockerCompose.java +++ b/docker-compose-rule-core/src/main/java/com/palantir/docker/compose/execution/DockerCompose.java @@ -44,6 +44,7 @@ static Version version() throws IOException, InterruptedException { Optional id(Container container) throws IOException, InterruptedException; String config() throws IOException, InterruptedException; List services() throws IOException, InterruptedException; + List servicesToStart(); boolean writeLogs(String container, OutputStream output) throws IOException; Ports ports(String service) throws IOException, InterruptedException; } diff --git a/docker-compose-rule-core/src/main/java/com/palantir/docker/compose/logging/FileLogCollector.java b/docker-compose-rule-core/src/main/java/com/palantir/docker/compose/logging/FileLogCollector.java index 9edba04cb..2a3bf7fcd 100644 --- a/docker-compose-rule-core/src/main/java/com/palantir/docker/compose/logging/FileLogCollector.java +++ b/docker-compose-rule-core/src/main/java/com/palantir/docker/compose/logging/FileLogCollector.java @@ -61,6 +61,13 @@ public synchronized void startCollecting(DockerCompose dockerCompose) throws IOE if (serviceNames.size() == 0) { return; } + + List servicesToStart = dockerCompose.servicesToStart(); + if (!servicesToStart.isEmpty()) { + // Services to start up are a subset of all services. + serviceNames = servicesToStart; + } + executor = Executors.newFixedThreadPool(serviceNames.size()); serviceNames.stream().forEachOrdered(service -> this.collectLogs(service, dockerCompose)); } diff --git a/docker-compose-rule-core/src/test/java/com/palantir/docker/compose/logging/FileLogCollectorShould.java b/docker-compose-rule-core/src/test/java/com/palantir/docker/compose/logging/FileLogCollectorShould.java index ae0b2d04c..0bd05e9dc 100644 --- a/docker-compose-rule-core/src/test/java/com/palantir/docker/compose/logging/FileLogCollectorShould.java +++ b/docker-compose-rule-core/src/test/java/com/palantir/docker/compose/logging/FileLogCollectorShould.java @@ -20,19 +20,23 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.arrayContaining; import static org.hamcrest.Matchers.arrayContainingInAnyOrder; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.emptyArray; import static org.hamcrest.core.Is.is; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; import com.palantir.docker.compose.execution.DockerCompose; import java.io.File; import java.io.IOException; import java.io.OutputStream; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.apache.commons.io.IOUtils; @@ -198,6 +202,29 @@ public void throw_exception_when_trying_to_start_a_started_collector_a_second_ti logCollector.startCollecting(compose); } + @Test + public void collect_logs_for_partial_services() throws IOException, InterruptedException { + when(compose.services()).thenReturn(ImmutableList.of("db", "db2")); + when(compose.servicesToStart()).thenReturn(ImmutableList.of("db")); + + List exceptions = Lists.newArrayList(); + CountDownLatch latch = new CountDownLatch(1); + when(compose.writeLogs(anyString(), any(OutputStream.class))).thenAnswer((args) -> { + String container = (String) args.getArguments()[0]; + if (!"db".equals(container)) { + exceptions.add(new IOException("Logs shouldn't be written for container: " + container)); + } else { + latch.countDown(); + } + return true; + }); + + logCollector.startCollecting(compose); + assertThat(latch.await(1, TimeUnit.SECONDS), is(true)); + logCollector.stopCollecting(); + assertThat(exceptions, is(empty())); + } + private static File cannotBeCreatedDirectory() { File cannotBeCreatedDirectory = mock(File.class); when(cannotBeCreatedDirectory.isFile()).thenReturn(false); diff --git a/docker-compose-rule-junit4/src/main/java/com/palantir/docker/compose/DockerComposeRule.java b/docker-compose-rule-junit4/src/main/java/com/palantir/docker/compose/DockerComposeRule.java index b05c59402..43509f479 100644 --- a/docker-compose-rule-junit4/src/main/java/com/palantir/docker/compose/DockerComposeRule.java +++ b/docker-compose-rule-junit4/src/main/java/com/palantir/docker/compose/DockerComposeRule.java @@ -68,6 +68,15 @@ public ProjectName projectName() { return ProjectName.random(); } + /** + * The names of services to start, via {@code docker-compose up -d SERVICE...}. If empty (the + * default), all services are started. This can be a subset of the services defined in + * the Docker Compose files. + * + * @return the names of services to start + */ + public abstract List servicesToStart(); + @Value.Default public DockerComposeExecutable dockerComposeExecutable() { return DockerComposeExecutable.builder() @@ -96,7 +105,7 @@ public ShutdownStrategy shutdownStrategy() { @Value.Default public DockerCompose dockerCompose() { - DockerCompose dockerCompose = new DefaultDockerCompose(dockerComposeExecutable(), machine()); + DockerCompose dockerCompose = new DefaultDockerCompose(dockerComposeExecutable(), machine(), servicesToStart()); return new RetryingDockerCompose(retryAttempts(), dockerCompose); } diff --git a/docker-compose-rule-junit4/src/test/java/com/palantir/docker/compose/DockerComposeRulePartialServicesIntegrationTest.java b/docker-compose-rule-junit4/src/test/java/com/palantir/docker/compose/DockerComposeRulePartialServicesIntegrationTest.java new file mode 100644 index 000000000..f931fa51c --- /dev/null +++ b/docker-compose-rule-junit4/src/test/java/com/palantir/docker/compose/DockerComposeRulePartialServicesIntegrationTest.java @@ -0,0 +1,101 @@ +/* + * 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 static com.google.common.base.Throwables.propagate; +import static com.palantir.docker.compose.connection.waiting.HealthChecks.toHaveAllPortsOpen; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +import com.google.common.collect.ImmutableList; +import com.palantir.docker.compose.configuration.DockerComposeFiles; +import com.palantir.docker.compose.connection.Container; +import com.palantir.docker.compose.execution.DockerExecutionException; +import java.io.IOException; +import java.util.List; +import java.util.function.Consumer; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class DockerComposeRulePartialServicesIntegrationTest { + + private static final List CONTAINERS = ImmutableList.of("db", "db2", "db3", "db4"); + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Rule + public final DockerComposeRule docker = DockerComposeRule.builder() + .files(DockerComposeFiles.from("src/test/resources/docker-compose.yaml")) + .addServicesToStart("db") // Only start the db container. + .waitingForService("db", toHaveAllPortsOpen()) + .build(); + + // Not a @Rule so that we can call before() and handle the exception. + // db5 is a nonexistent service in docker-compose.yaml. + private final DockerComposeRule invalidDocker = DockerComposeRule.builder() + .files(DockerComposeFiles.from("src/test/resources/docker-compose.yaml")) + .addServicesToStart("db5") // Only start the db container. + .waitingForService("db5", toHaveAllPortsOpen()) + .build(); + + private static void forEachContainer(Consumer consumer) { + CONTAINERS.forEach(consumer); + } + + @Test + public void should_run_docker_compose_up_for_db_container_only() { + forEachContainer(containerName -> { + Container container = docker.containers().container(containerName); + try { + if ("db".equals(containerName)) { + assertThat(container.state().isUp(), is(true)); + assertThat(container.port(5432).isListeningNow(), is(true)); + } else { + assertThat(container.state().isUp(), is(false)); + } + } catch (IOException | InterruptedException e) { + propagate(e); + } + }); + } + + @Test + public void after_test_is_executed_the_launched_db_container_is_no_longer_listening() { + docker.after(); + + forEachContainer(containerName -> { + Container container = docker.containers().container(containerName); + try { + assertThat(container.state().isUp(), is(false)); + if ("db".equals(containerName)) { + assertThat(container.port(5432).isListeningNow(), is(false)); + } + } catch (IOException | InterruptedException e) { + propagate(e); + } + }); + } + + @Test + public void should_run_docker_compose_up_for_nonexistent_container() throws IOException, InterruptedException { + exception.expect(DockerExecutionException.class); + exception.expectMessage("No such service: db5"); + + invalidDocker.before(); + } +}