Skip to content

Commit

Permalink
Docker compose v2 (#722)
Browse files Browse the repository at this point in the history
docker-compose-rule now adds (opt in) support for docker-compose v2
  • Loading branch information
mswintermeyer authored Mar 23, 2023
1 parent 916c91a commit 98c4b39
Show file tree
Hide file tree
Showing 20 changed files with 249 additions and 159 deletions.
5 changes: 5 additions & 0 deletions changelog/@unreleased/pr-722.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type: feature
feature:
description: docker-compose-rule now adds (opt in) support for docker-compose v2
links:
- https://github.com/palantir/docker-compose-rule/pull/722
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* (c) Copyright 2019 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 org.assertj.core.api.Assertions.assertThat;

import com.palantir.docker.compose.configuration.DockerComposeFiles;
import com.palantir.docker.compose.connection.waiting.HealthChecks;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

public class DockerComposeExtensionV2IntegrationTest {

@RegisterExtension
public static final DockerComposeExtension docker = DockerComposeExtension.builder()
.files(DockerComposeFiles.from("src/integrationTest/resources/docker-compose.yaml"))
.waitingForService("db", HealthChecks.toHaveAllPortsOpen())
.waitingForService("db2", HealthChecks.toHaveAllPortsOpen())
.waitingForService("db3", HealthChecks.toHaveAllPortsOpen())
.waitingForService("db4", HealthChecks.toHaveAllPortsOpen())
.useDockerComposeV2(true)
.build();

private static int port;

@Test
@Order(1)
public void an_external_port_exists() {
port = docker.containers().container("db").port(5432).getExternalPort();
assertThat(port).isNotZero();
}

@Test
@Order(2)
public void container_stays_up_between_tests() {
assertThat(docker.containers().container("db").port(5432).getExternalPort())
.isEqualTo(port);
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
db:
image: appropriate/nc
command: /bin/sh -c 'echo server started && nc -lk 5432'
ports:
- "5432"
version: '2.1'

db2:
image: appropriate/nc
command: /bin/sh -c 'echo server started && nc -lk 5432'
ports:
- "5432"
services:
db:
image: appropriate/nc
command: /bin/sh -c 'echo server started && nc -lk 5432'
ports:
- "5432"

db3:
image: appropriate/nc
command: /bin/sh -c 'echo server started && nc -lk 5432'
ports:
- "5432"
db2:
image: appropriate/nc
command: /bin/sh -c 'echo server started && nc -lk 5432'
ports:
- "5432"

db4:
image: appropriate/nc
command: /bin/sh -c 'echo server started && nc -lk 5432'
ports:
- "5432"
db3:
image: appropriate/nc
command: /bin/sh -c 'echo server started && nc -lk 5432'
ports:
- "5432"

db4:
image: appropriate/nc
command: /bin/sh -c 'echo server started && nc -lk 5432'
ports:
- "5432"
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,10 @@
import static com.palantir.docker.compose.execution.DockerComposeExecArgument.arguments;
import static com.palantir.docker.compose.execution.DockerComposeExecOption.noOptions;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeThat;

import com.github.zafarkhaja.semver.Version;
import com.palantir.docker.compose.connection.Container;
import com.palantir.docker.compose.connection.State;
import com.palantir.docker.compose.execution.Docker;
import com.palantir.docker.compose.execution.DockerCompose;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
Expand All @@ -53,19 +48,9 @@ public void shutdownPool() {
}
}

/**
* This test is not currently enabled in Circle as it does not provide a
* sufficiently recent version of docker-compose.
*
* @see <a href="https://github.com/palantir/docker-compose-rule/issues/156">Issue #156</a>
*/
@Test
public void dockerComposeManagerWaitsUntilHealthcheckPasses()
throws ExecutionException, IOException, InterruptedException, TimeoutException {
assumeThat("docker version", Docker.version(), greaterThanOrEqualTo(Version.forIntegers(1, 12, 0)));
assumeThat(
"docker-compose version", DockerCompose.version(), greaterThanOrEqualTo(Version.forIntegers(1, 10, 0)));

docker = new DockerComposeManager.Builder()
.file("src/test/resources/native-healthcheck.yaml")
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,17 @@ public void produces_events_when_a_container_healthcheck_exceeds_its_timeout() {

List<Event> events = getEvents();

ClusterWaitEvent clusterWait = events.stream()
Optional<ClusterWaitEvent> clusterWait = events.stream()
.map(this::isClusterWait)
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst()
.orElseThrow(() -> new IllegalStateException("no clusterwaits in events"));
.filter(clusterWaitEvent ->
clusterWaitEvent.getTask().getFailure().isPresent())
.findFirst();

assertThat(clusterWait.getServiceNames()).containsOnly("one");
assertThat(clusterWait.getTask().getFailure()).hasValueSatisfying(failure -> {
assertThat(clusterWait).isPresent();
assertThat(clusterWait.get().getServiceNames()).containsOnly("one");
assertThat(clusterWait.get().getTask().getFailure()).hasValueSatisfying(failure -> {
assertThat(failure).contains(failureMessage);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,8 @@
import static com.palantir.docker.compose.execution.DockerComposeExecOption.noOptions;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assume.assumeThat;

import com.github.zafarkhaja.semver.Version;
import com.palantir.docker.compose.configuration.DockerComposeFiles;
import com.palantir.docker.compose.configuration.ProjectName;
import com.palantir.docker.compose.execution.DefaultDockerCompose;
Expand Down Expand Up @@ -59,18 +56,8 @@ public void testStateChanges_withoutHealthCheck() throws IOException, Interrupte
assertEquals(State.DOWN, container.state());
}

/**
* This test is not currently enabled in Circle as it does not provide a
* sufficiently recent version of docker-compose.
*
* @see <a href="https://github.com/palantir/docker-compose-rule/issues/156">Issue #156</a>
*/
@Test
public void testStateChanges_withHealthCheck() throws IOException, InterruptedException {
assumeThat("docker version", Docker.version(), greaterThanOrEqualTo(Version.forIntegers(1, 12, 0)));
assumeThat(
"docker-compose version", DockerCompose.version(), greaterThanOrEqualTo(Version.forIntegers(1, 10, 0)));

DockerCompose dockerCompose = new DefaultDockerCompose(
DockerComposeFiles.from("src/test/resources/native-healthcheck.yaml"),
dockerMachine,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ public void logs_can_be_saved_to_a_directory() throws IOException, InterruptedEx
}
assertThat(
new File(logFolder.getRoot(), "db.log"),
is(fileWithConents(matchingPattern(".*Attaching to \\w+_db_1.*server started.*"))));
is(fileWithConents(matchingPattern(".*db-1.*server started.*"))));
assertThat(
new File(logFolder.getRoot(), "db2.log"),
is(fileWithConents(matchingPattern(".*Attaching to \\w+_db2_1.*server started.*"))));
is(fileWithConents(matchingPattern(".*db2-1.*server started.*"))));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,15 @@ public DockerComposeExecutable dockerComposeExecutable() {
.dockerComposeFiles(files())
.dockerConfiguration(machine())
.projectName(projectName())
.useDockerComposeV2(useDockerComposeV2())
.build();
}

@Value.Default
public boolean useDockerComposeV2() {
return false;
}

@Value.Default
public DockerExecutable dockerExecutable() {
return DockerExecutable.builder().dockerConfiguration(machine()).build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,26 @@
import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.toList;

import com.google.common.base.Splitter;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Stream;

public final class ContainerNames {
private static final Pattern HEAD_PATTERN = Pattern.compile("-+(\r|\n)+");
private static final Pattern BODY_PATTERN = Pattern.compile("(\r|\n)+");
private static final String LINE_DELIM = "\n";

private ContainerNames() {}

public static List<ContainerName> parseFromDockerComposePs(String psOutput) {
List<String> psHeadAndBody = Splitter.on(HEAD_PATTERN).splitToList(psOutput);
List<String> psHeadAndBody = Arrays.asList(psOutput.split(LINE_DELIM));
if (psHeadAndBody.size() < 2) {
return emptyList();
}

String psBody = psHeadAndBody.get(1);
return psBodyLines(psBody).map(ContainerName::fromPsLine).collect(toList());
}
List<String> psBody = psHeadAndBody.subList(1, psHeadAndBody.size());

private static Stream<String> psBodyLines(String psBody) {
List<String> lines = Splitter.on(BODY_PATTERN).splitToList(psBody);
return lines.stream().map(String::trim).filter(line -> !line.isEmpty());
return psBody.stream()
.map(String::trim)
.filter(line -> !line.isEmpty())
.map(ContainerName::fromPsLine)
.collect(toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,15 @@ private void verifyDockerComposeVersionAtLeast(Version targetVersion, String mes
}

private Version version() throws IOException, InterruptedException {
String versionOutput = command.execute(Command.throwingOnError(), "-v");
return DockerComposeVersion.parseFromDockerComposeVersion(versionOutput);
// docker-compose and docker compose have different ways to query the version.
// Since it can be either one, even silently sym-linked underneath us, try both
try {
String versionOutput = command.execute(Command.throwingOnError(), "version");
return DockerComposeVersion.parseFromDockerComposeVersion(versionOutput);
} catch (RuntimeException _exception) {
String versionOutput = command.execute(Command.throwingOnError(), "-v");
return DockerComposeVersion.parseFromDockerComposeVersion(versionOutput);
}
}

private static String[] constructFullDockerComposeExecArguments(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,31 @@
public abstract class DockerComposeExecutable implements Executable {
private static final Logger log = LoggerFactory.getLogger(DockerComposeExecutable.class);

private static final DockerCommandLocations DOCKER_COMPOSE_LOCATIONS = new DockerCommandLocations(
private static final DockerCommandLocations DOCKER_COMPOSE_V1_LOCATIONS = new DockerCommandLocations(
System.getenv("DOCKER_COMPOSE_LOCATION"), "/usr/local/bin/docker-compose", "/usr/bin/docker-compose");

private static String defaultDockerComposePath() {
String pathToUse = DOCKER_COMPOSE_LOCATIONS
private static final DockerCommandLocations DOCKER_COMPOSE_V2_LOCATIONS =
new DockerCommandLocations(System.getenv("DOCKER_LOCATION"), "/usr/local/bin/docker", "/usr/bin/docker");

private static String defaultDockerComposePath(boolean useDockerComposeV2) {
DockerCommandLocations locations =
useDockerComposeV2 ? DOCKER_COMPOSE_V2_LOCATIONS : DOCKER_COMPOSE_V1_LOCATIONS;
String pathToUse = locations
.preferredLocation()
.orElseThrow(() -> new IllegalStateException(
"Could not find docker-compose, looked in: " + DOCKER_COMPOSE_LOCATIONS));
.orElseThrow(() -> new IllegalStateException("Could not find docker-compose, looked in: " + locations));

log.debug("Using docker-compose found at " + pathToUse);

return pathToUse;
}

/**
* Returns the version of docker-compose.
*
* @deprecated only supports getting the version of docker-compose v1.
* Just use {@link #execute} to get the version directly.
*/
@Deprecated
static Version version() throws IOException, InterruptedException {
Command dockerCompose = new Command(
new Executable() {
Expand All @@ -55,7 +66,7 @@ public String commandName() {
@Override
public Process execute(String... commands) throws IOException {
List<String> args = ImmutableList.<String>builder()
.add(defaultDockerComposePath())
.add(defaultDockerComposePath(false))
.add(commands)
.build();
return new ProcessBuilder(args)
Expand All @@ -82,20 +93,30 @@ public ProjectName projectName() {

@Override
public final String commandName() {
return "docker-compose";
return useDockerComposeV2() ? "docker compose" : "docker-compose";
}

@Value.Derived
protected String dockerComposePath() {
return defaultDockerComposePath();
protected List<String> dockerComposePath() {
String path = defaultDockerComposePath(useDockerComposeV2());
if (useDockerComposeV2()) {
return ImmutableList.of(path, "compose");
} else {
return ImmutableList.of(path);
}
}

@Value.Default
public boolean useDockerComposeV2() {
return false;
}

@Override
public Process execute(String... commands) throws IOException {
DockerForMacHostsIssue.issueWarning();

List<String> args = ImmutableList.<String>builder()
.add(dockerComposePath())
.addAll(dockerComposePath())
.addAll(projectName().constructComposeFileCommand())
.addAll(dockerComposeFiles().constructComposeFileCommand())
.add(commands)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,24 @@
package com.palantir.docker.compose.execution;

import com.github.zafarkhaja.semver.Version;
import com.google.common.base.Splitter;
import java.util.List;

public final class DockerComposeVersion {

private DockerComposeVersion() {}

private static final String VERSION_PREFIX = " version ";

// docker-compose version format is like 1.7.0rc1, which can't be parsed by java-semver
// here we only pass 1.7.0 to java-semver
public static Version parseFromDockerComposeVersion(String versionOutput) {
List<String> splitOnSeparator = Splitter.on(' ').splitToList(versionOutput);
String version = splitOnSeparator.get(2);
String version = versionOutput.substring(versionOutput.indexOf(VERSION_PREFIX) + VERSION_PREFIX.length());
StringBuilder builder = new StringBuilder();

// Handle new version format, e.g. v2.1.0
if (version.startsWith("v")) {
version = version.substring(1);
}

for (int i = 0; i < version.length(); i++) {
if ((version.charAt(i) >= '0' && version.charAt(i) <= '9') || version.charAt(i) == '.') {
builder.append(version.charAt(i));
Expand Down
Loading

0 comments on commit 98c4b39

Please sign in to comment.