Skip to content

Commit

Permalink
Merge pull request #59 from palantir/feature/use-host-networked-ports
Browse files Browse the repository at this point in the history
Feature/use host networked ports
  • Loading branch information
joelea committed May 9, 2016
2 parents b4c3278 + ab1c672 commit 7b4206a
Show file tree
Hide file tree
Showing 26 changed files with 354 additions and 353 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public class DockerCompositionTest {
.waitingForService("web", HealthChecks.toRespondOverHttp(8080, (port) -> port.inFormat("https://$HOST:$EXTERNAL_PORT")))
.waitingForService("other", (container) -> customServiceCheck(container), Duration.standardMinutes(2))
.waitingForServices(ImmutableList.of("node1", "node2"), toBeHealthyAsACluster())
.waitingForHostNetworkedPort(5432, toBeOpen())
.build();

@Test
Expand All @@ -88,8 +89,9 @@ public class DockerCompositionTest {
}
```

The entrypoint method `waitingForService(String container, SingleServiceHealthCheck check[, Duration timeout])` will make sure the healthcheck passes for that container before the tests start.
The entrypoint method `waitingForServices(List<String> containers, MultiServiceHealthCheck check[, Duration timeout])` will make sure the healthcheck passes for the cluster of containers before the tests start.
The entrypoint method `waitingForService(String container, HealthCheck<Container> check[, Duration timeout])` will make sure the healthcheck passes for that container before the tests start.
The entrypoint method `waitingForServices(List<String> containers, HealthCheck<List<Container>> check[, Duration timeout])` will make sure the healthcheck passes for the cluster of containers before the tests start.
The entrypoint method `waitingForHostNetworkedPort(int portNumber, HealthCheck<DockerPort> check[, Duration timeout])` will make sure the healthcheck passes for a particular host networked port.

We provide 2 default healthChecks in the HealthChecks class:

Expand Down
6 changes: 6 additions & 0 deletions circle.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
machine:
pre:
- curl -sSL https://s3.amazonaws.com/circle-downloads/install-circleci-docker.sh | bash -s -- 1.10.0
java:
version: oraclejdk8
services:
- docker
environment:
TERM: dumb

dependencies:
pre:
- sudo pip install docker-compose

test:
pre:
- ./gradlew findbugsMain findbugsTest checkstyleMain checkstyleTest javadoc --info
Expand Down
48 changes: 35 additions & 13 deletions src/main/java/com/palantir/docker/compose/DockerComposeRule.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@
*/
package com.palantir.docker.compose;

import static com.palantir.docker.compose.connection.waiting.ClusterHealthCheck.serviceHealthCheck;
import static com.palantir.docker.compose.connection.waiting.ClusterHealthCheck.transformingHealthCheck;

import com.palantir.docker.compose.configuration.DockerComposeFiles;
import com.palantir.docker.compose.configuration.ProjectName;
import com.palantir.docker.compose.connection.Cluster;
import com.palantir.docker.compose.connection.Container;
import com.palantir.docker.compose.connection.ContainerCache;
import com.palantir.docker.compose.connection.DockerMachine;
import com.palantir.docker.compose.connection.DockerPort;
import com.palantir.docker.compose.connection.ImmutableCluster;
import com.palantir.docker.compose.connection.waiting.ClusterHealthCheck;
import com.palantir.docker.compose.connection.waiting.ClusterWait;
import com.palantir.docker.compose.connection.waiting.MultiServiceHealthCheck;
import com.palantir.docker.compose.connection.waiting.MultiServiceWait;
import com.palantir.docker.compose.connection.waiting.SingleServiceHealthCheck;
import com.palantir.docker.compose.connection.waiting.SingleServiceWait;
import com.palantir.docker.compose.connection.waiting.HealthCheck;
import com.palantir.docker.compose.execution.DefaultDockerCompose;
import com.palantir.docker.compose.execution.DockerCompose;
import com.palantir.docker.compose.execution.DockerComposeExecArgument;
Expand All @@ -38,6 +42,10 @@ public abstract class DockerComposeRule extends ExternalResource {

private static final Logger log = LoggerFactory.getLogger(DockerComposeRule.class);

public DockerPort hostNetworkedPort(int port) {
return new DockerPort(machine().getIp(), port, port);
}

public abstract DockerComposeFiles files();

protected abstract List<ClusterWait> clusterWaits();
Expand Down Expand Up @@ -69,7 +77,10 @@ public DockerCompose dockerCompose() {

@Value.Default
public Cluster containers() {
return new ContainerCache(dockerCompose());
return ImmutableCluster.builder()
.ip(machine().getIp())
.containerCache(new ContainerCache(dockerCompose()))
.build();
}

@Value.Default
Expand Down Expand Up @@ -149,20 +160,31 @@ public ImmutableDockerComposeRule.Builder saveLogsTo(String path) {

public abstract ImmutableDockerComposeRule.Builder addClusterWait(ClusterWait clusterWait);

public ImmutableDockerComposeRule.Builder waitingForService(String serviceName, SingleServiceHealthCheck healthCheck) {
return addClusterWait(SingleServiceWait.of(serviceName, healthCheck, DEFAULT_TIMEOUT));
public ImmutableDockerComposeRule.Builder waitingForService(String serviceName, HealthCheck<Container> healthCheck) {
return waitingForService(serviceName, healthCheck, DEFAULT_TIMEOUT);
}

public ImmutableDockerComposeRule.Builder waitingForService(String serviceName, HealthCheck<Container> healthCheck, Duration timeout) {
ClusterHealthCheck clusterHealthCheck = serviceHealthCheck(serviceName, healthCheck);
return addClusterWait(new ClusterWait(clusterHealthCheck, timeout));
}

public ImmutableDockerComposeRule.Builder waitingForServices(List<String> services, HealthCheck<List<Container>> healthCheck) {
return waitingForServices(services, healthCheck, DEFAULT_TIMEOUT);
}

public ImmutableDockerComposeRule.Builder waitingForService(String serviceName, SingleServiceHealthCheck healthCheck, Duration timeout) {
return addClusterWait(SingleServiceWait.of(serviceName, healthCheck, timeout));
public ImmutableDockerComposeRule.Builder waitingForServices(List<String> services, HealthCheck<List<Container>> healthCheck, Duration timeout) {
ClusterHealthCheck clusterHealthCheck = serviceHealthCheck(services, healthCheck);
return addClusterWait(new ClusterWait(clusterHealthCheck, timeout));
}

public ImmutableDockerComposeRule.Builder waitingForServices(List<String> services, MultiServiceHealthCheck healthCheck) {
return addClusterWait(MultiServiceWait.of(services, healthCheck, DEFAULT_TIMEOUT));
public ImmutableDockerComposeRule.Builder waitingForHostNetworkedPort(int port, HealthCheck<DockerPort> healthCheck) {
return waitingForHostNetworkedPort(port, healthCheck, DEFAULT_TIMEOUT);
}

public ImmutableDockerComposeRule.Builder waitingForServices(List<String> services, MultiServiceHealthCheck healthCheck, Duration timeout) {
return addClusterWait(MultiServiceWait.of(services, healthCheck, timeout));
public ImmutableDockerComposeRule.Builder waitingForHostNetworkedPort(int port, HealthCheck<DockerPort> healthCheck, Duration timeout) {
ClusterHealthCheck clusterHealthCheck = transformingHealthCheck(cluster -> new DockerPort(cluster.ip(), port, port), healthCheck);
return addClusterWait(new ClusterWait(clusterHealthCheck, timeout));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,7 @@ public void exec(DockerComposeExecOption options, String containerName, DockerCo
rule.exec(options, containerName, arguments);
}

public DockerPort hostNetworkedPort(int port) {
return rule.hostNetworkedPort(port);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
import com.palantir.docker.compose.ImmutableDockerComposeRule.Builder;
import com.palantir.docker.compose.configuration.DockerComposeFiles;
import com.palantir.docker.compose.configuration.ProjectName;
import com.palantir.docker.compose.connection.Container;
import com.palantir.docker.compose.connection.DockerMachine;
import com.palantir.docker.compose.connection.waiting.MultiServiceHealthCheck;
import com.palantir.docker.compose.connection.waiting.SingleServiceHealthCheck;
import com.palantir.docker.compose.connection.DockerPort;
import com.palantir.docker.compose.connection.waiting.HealthCheck;
import com.palantir.docker.compose.execution.DockerCompose;
import java.util.List;
import org.joda.time.Duration;
Expand All @@ -32,26 +33,36 @@ public DockerCompositionBuilder() {
this.builder = DockerComposeRule.builder();
}

public DockerCompositionBuilder waitingForService(String serviceName, SingleServiceHealthCheck check) {
public DockerCompositionBuilder waitingForService(String serviceName, HealthCheck<Container> check) {
builder.waitingForService(serviceName, check);
return this;
}

public DockerCompositionBuilder waitingForServices(List<String> services, MultiServiceHealthCheck check) {
public DockerCompositionBuilder waitingForServices(List<String> services, HealthCheck<List<Container>> check) {
builder.waitingForServices(services, check);
return this;
}

public DockerCompositionBuilder waitingForServices(List<String> services, MultiServiceHealthCheck check, Duration timeout) {
public DockerCompositionBuilder waitingForServices(List<String> services, HealthCheck<List<Container>> check, Duration timeout) {
builder.waitingForServices(services, check, timeout);
return this;
}

public DockerCompositionBuilder waitingForService(String serviceName, SingleServiceHealthCheck check, Duration timeout) {
public DockerCompositionBuilder waitingForService(String serviceName, HealthCheck<Container> check, Duration timeout) {
builder.waitingForService(serviceName, check, timeout);
return this;
}

public DockerCompositionBuilder waitingForHostNetworkedPort(int port, HealthCheck<DockerPort> healthCheck, Duration timeout) {
builder.waitingForHostNetworkedPort(port, healthCheck, timeout);
return this;
}

public DockerCompositionBuilder waitingForHostNetworkedPort(int port, HealthCheck<DockerPort> healthCheck) {
builder.waitingForHostNetworkedPort(port, healthCheck);
return this;
}

public DockerCompositionBuilder files(DockerComposeFiles files) {
builder.files(files);
return this;
Expand Down
21 changes: 19 additions & 2 deletions src/main/java/com/palantir/docker/compose/connection/Cluster.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,25 @@

package com.palantir.docker.compose.connection;

public interface Cluster {
import static java.util.stream.Collectors.toList;

Container container(String name);
import java.util.List;
import org.immutables.value.Value;

@Value.Immutable
public abstract class Cluster {

public abstract String ip();
public abstract ContainerCache containerCache();

public Container container(String name) {
return containerCache().container(name);
}

public List<Container> containers(List<String> containerNames) {
return containerNames.stream()
.map(this::container)
.collect(toList());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import java.util.HashMap;
import java.util.Map;

public class ContainerCache implements Cluster {
public class ContainerCache {

private final Map<String, Container> containers = new HashMap<>();
private final DockerCompose dockerCompose;
Expand All @@ -28,7 +28,6 @@ public ContainerCache(DockerCompose dockerCompose) {
this.dockerCompose = dockerCompose;
}

@Override
public Container container(String containerName) {
containers.putIfAbsent(containerName, dockerCompose.container(containerName));
return containers.get(containerName);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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.connection.waiting;

import com.palantir.docker.compose.connection.Cluster;
import com.palantir.docker.compose.connection.Container;
import java.util.List;
import java.util.function.Function;

@FunctionalInterface
public interface ClusterHealthCheck {
static ClusterHealthCheck serviceHealthCheck(List<String> containerNames, HealthCheck<List<Container>> delegate) {
return transformingHealthCheck(cluster -> cluster.containers(containerNames), delegate);
}

static ClusterHealthCheck serviceHealthCheck(String containerName, HealthCheck<Container> containerCheck) {
return transformingHealthCheck(cluster -> cluster.container(containerName), containerCheck);
}

static <T> ClusterHealthCheck transformingHealthCheck(Function<Cluster, T> transform, HealthCheck<T> healthCheck) {
return cluster -> {
T target = transform.apply(cluster);
return healthCheck.isHealthy(target);
};
}

SuccessOrFailure isClusterHealthy(Cluster cluster);
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,72 @@
/*
* 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.connection.waiting;

import com.jayway.awaitility.Awaitility;
import com.jayway.awaitility.core.ConditionTimeoutException;
import com.palantir.docker.compose.connection.Cluster;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.joda.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ClusterWait {
private static final Logger log = LoggerFactory.getLogger(ClusterWait.class);
private final ClusterHealthCheck clusterHealthCheck;
private final Duration timeout;

public ClusterWait(ClusterHealthCheck clusterHealthCheck, Duration timeout) {
this.clusterHealthCheck = clusterHealthCheck;
this.timeout = timeout;
}

public void waitUntilReady(Cluster cluster) {
final AtomicReference<Optional<SuccessOrFailure>> lastSuccessOrFailure = new AtomicReference<>(
Optional.empty());

log.debug("Waiting for cluster to be healthy");
try {
Awaitility.await()
.pollInterval(50, TimeUnit.MILLISECONDS)
.atMost(timeout.getMillis(), TimeUnit.MILLISECONDS)
.until(weHaveSuccess(cluster, lastSuccessOrFailure));
} catch (ConditionTimeoutException e) {
throw new IllegalStateException(serviceDidNotStartupExceptionMessage(lastSuccessOrFailure));
}
}

private Callable<Boolean> weHaveSuccess(Cluster cluster,
AtomicReference<Optional<SuccessOrFailure>> lastSuccessOrFailure) {
return () -> {
SuccessOrFailure successOrFailure = clusterHealthCheck.isClusterHealthy(cluster);
lastSuccessOrFailure.set(Optional.of(successOrFailure));
return successOrFailure.succeeded();
};
}

@FunctionalInterface
public interface ClusterWait {
private String serviceDidNotStartupExceptionMessage(
AtomicReference<Optional<SuccessOrFailure>> lastSuccessOrFailure) {
String healthcheckFailureMessage = lastSuccessOrFailure.get()
.flatMap(SuccessOrFailure::toOptionalFailureMessage)
.orElse("The healthcheck did not finish before the timeout");

void waitUntilReady(Cluster cluster);
return "The cluster failed to pass a startup check: " + healthcheckFailureMessage;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.palantir.docker.compose.connection.waiting;

import com.palantir.docker.compose.connection.Container;

@FunctionalInterface
public interface SingleServiceHealthCheck {
SuccessOrFailure isServiceUp(Container container);
public interface HealthCheck<T> {
SuccessOrFailure isHealthy(T target);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ public final class HealthChecks {

private HealthChecks() {}

public static SingleServiceHealthCheck toRespondOverHttp(int internalPort, Function<DockerPort, String> urlFunction) {
public static HealthCheck<Container> toRespondOverHttp(int internalPort, Function<DockerPort, String> urlFunction) {
return container -> container.portIsListeningOnHttp(internalPort, urlFunction);
}

public static SingleServiceHealthCheck toHaveAllPortsOpen() {
public static HealthCheck<Container> toHaveAllPortsOpen() {
return Container::areAllPortsOpen;
}
}
Loading

0 comments on commit 7b4206a

Please sign in to comment.