Skip to content

Commit

Permalink
Support for WaitStrategy on DockerComposeContainer - alternative appr…
Browse files Browse the repository at this point in the history
…oach (#600)

* Support for WaitStrategy on DockerComposeContainer

As discussed in #174

* CI code quality fixes

* CI code quality fixes

* Another attempt at adding WaitStrategy support for docker-compose

* Another attempt at adding WaitStrategy support for docker-compose

fixed some tests and added some files missed from previous commit

* Changes after code review comments

Added new WaitStrategyTarget interface, plus moved some methods into
new LogFollower and CommandExecutor interfaces

* Changes after code review comments

Removed CommandExecutor and LogFollower interfaces.
Added ExecInContainerPattern utility class.

Changed existing WaitStrategy implementations to inherit from new ones

* CI code quality fixes

* Changes from code review.

Moved getMappedPort to default implementation in ContainerState.
Using proxyContainer in ComposeStrategyWaitServiceTarget to get ip.
Removed unnecessary null checks.
Using lazy getter for containerInfo in ComposeStrategyWaitServiceTarget.

* Changes from code review.

Removed getLogger() method.
Removed getContainerName from Container and ContainerState.
Removed getExposedPortNumbers from ContainerState.
Added waitingFor method to DockerComposeContainer.
Removed StartupTimeout interface.
  • Loading branch information
barrycommins authored and bsideup committed Mar 19, 2018
1 parent c2116ca commit 8558203
Show file tree
Hide file tree
Showing 33 changed files with 1,203 additions and 472 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ All notable changes to this project will be documented in this file.
- Abstracted and changed database init script functionality to support use of SQL-like scripts with non-JDBC connections. ([\#551](https://github.com/testcontainers/testcontainers-java/pull/551))
- Added `JdbcDatabaseContainer(Future)` constructor. ([\#543](https://github.com/testcontainers/testcontainers-java/issues/543))
- Mark DockerMachineClientProviderStrategy as not persistable ([\#593](https://github.com/testcontainers/testcontainers-java/pull/593))
- Added `waitingFor(String serviceName, WaitStrategy waitStrategy)` and overloaded `withExposedService()` methods to `DockerComposeContainer` to allow user to define `WaitStrategy` for compose containers. ([\#174](https://github.com/testcontainers/testcontainers-java/issues/174) and [\#515](https://github.com/testcontainers/testcontainers-java/issues/515))
- Deprecated `WaitStrategy` and implementations in favour of classes with same names in `org.testcontainers.containers.strategy`
- Added `ContainerState` interface representing the state of a started container
- Added `WaitStrategyTarget` interface which is the target of the new `WaitStrategy`

## [1.6.0] - 2018-01-28

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package org.testcontainers.containers;

import com.github.dockerjava.api.command.InspectContainerResponse;
import com.github.dockerjava.api.model.Container;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NonNull;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.wait.strategy.WaitStrategyTarget;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


/**
* Class to provide a wait strategy target for services started through docker-compose
*/
@EqualsAndHashCode
class ComposeServiceWaitStrategyTarget implements WaitStrategyTarget {

private final Container container;
private final GenericContainer proxyContainer;
@NonNull
private Map<Integer, Integer> mappedPorts;
@Getter(lazy=true)
private final InspectContainerResponse containerInfo = DockerClientFactory.instance().client().inspectContainerCmd(getContainerId()).exec();

ComposeServiceWaitStrategyTarget(Container container, GenericContainer proxyContainer,
@NonNull Map<Integer, Integer> mappedPorts) {
this.container = container;
this.proxyContainer = proxyContainer;
this.mappedPorts = new HashMap<>(mappedPorts);
}

/**
* {@inheritDoc}
*/
@Override
public List<Integer> getExposedPorts() {
return new ArrayList<>(this.mappedPorts.keySet());
}

/**
* {@inheritDoc}
*/
@Override
public Integer getMappedPort(int originalPort) {
return this.proxyContainer.getMappedPort(this.mappedPorts.get(originalPort));
}

/**
* {@inheritDoc}
*/
@Override
public String getContainerIpAddress() {
return proxyContainer.getContainerIpAddress();
}

/**
* {@inheritDoc}
*/
@Override
public String getContainerId() {
return this.container.getId();
}
}
78 changes: 14 additions & 64 deletions core/src/main/java/org/testcontainers/containers/Container.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package org.testcontainers.containers;

import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.InspectContainerResponse;
import com.github.dockerjava.api.model.Bind;
import com.github.dockerjava.api.model.Info;
import lombok.NonNull;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.containers.startupcheck.StartupCheckStrategy;
import org.testcontainers.containers.traits.LinkableContainer;
import org.testcontainers.containers.wait.Wait;
import org.testcontainers.containers.wait.WaitStrategy;
import org.testcontainers.containers.wait.strategy.WaitStrategy;
import org.testcontainers.utility.LogUtils;
import org.testcontainers.utility.MountableFile;

import java.io.IOException;
Expand All @@ -22,7 +22,7 @@
import java.util.function.Consumer;
import java.util.function.Function;

public interface Container<SELF extends Container<SELF>> extends LinkableContainer {
public interface Container<SELF extends Container<SELF>> extends LinkableContainer, ContainerState {

/**
* @return a reference to this container instance, cast to the expected generic type.
Expand Down Expand Up @@ -131,7 +131,7 @@ default void addFileSystemBind(final String hostPath, final String containerPath
/**
* Specify the {@link WaitStrategy} to use to determine if the container is ready.
*
* @see Wait#defaultWaitStrategy()
* @see org.testcontainers.containers.wait.strategy.Wait#defaultWaitStrategy()
* @param waitStrategy the WaitStrategy to use
* @return this
*/
Expand Down Expand Up @@ -283,7 +283,7 @@ default SELF withClasspathResourceMapping(final String resourcePath, final Strin

/**
* Set the duration of waiting time until container treated as started.
* @see WaitStrategy#waitUntilReady(GenericContainer)
* @see WaitStrategy#waitUntilReady(org.testcontainers.containers.wait.strategy.WaitStrategyTarget)
*
* @param startupTimeout timeout
* @return this
Expand All @@ -297,13 +297,6 @@ default SELF withClasspathResourceMapping(final String resourcePath, final Strin
*/
SELF withPrivilegedMode(boolean mode);

/**
* Get the IP address that this container may be reached on (may not be the local machine).
*
* @return an IP address
*/
String getContainerIpAddress();

/**
* Only consider a container to have successfully started if it has been running for this duration. The default
* value is null; if that's the value, ignore this check.
Expand All @@ -327,33 +320,6 @@ default SELF withClasspathResourceMapping(final String resourcePath, final Strin
*/
SELF withWorkingDirectory(String workDir);

/**
* @return is the container currently running?
*/
Boolean isRunning();

/**
* Get the actual mapped port for a first port exposed by the container.
*
* @return the port that the exposed port is mapped to
* @throws IllegalStateException if there are no exposed ports
*/
default Integer getFirstMappedPort() {
return getExposedPorts()
.stream()
.findFirst()
.map(this::getMappedPort)
.orElseThrow(() -> new IllegalStateException("Container doesn't expose any ports"));
}

/**
* Get the actual mapped port for a given port exposed by the container.
*
* @param originalPort the original TCP port that is exposed
* @return the port that the exposed port is mapped to, or null if it is not exposed
*/
Integer getMappedPort(int originalPort);

/**
* <b>Resolve</b> Docker image and set it.
*
Expand Down Expand Up @@ -386,7 +352,9 @@ default Integer getFirstMappedPort() {
*
* @param consumer consumer that the frames should be sent to
*/
void followOutput(Consumer<OutputFrame> consumer);
default void followOutput(Consumer<OutputFrame> consumer) {
LogUtils.followOutput(DockerClientFactory.instance().client(), getContainerId(), consumer);
}

/**
* Follow container output, sending each frame (usually, line) to a consumer. This method allows Stdout and/or stderr
Expand All @@ -395,7 +363,9 @@ default Integer getFirstMappedPort() {
* @param consumer consumer that the frames should be sent to
* @param types types that should be followed (one or both of STDOUT, STDERR)
*/
void followOutput(Consumer<OutputFrame> consumer, OutputFrame.OutputType... types);
default void followOutput(Consumer<OutputFrame> consumer, OutputFrame.OutputType... types) {
LogUtils.followOutput(DockerClientFactory.instance().client(), getContainerId(), consumer, types);
}


/**
Expand All @@ -419,22 +389,15 @@ default Integer getFirstMappedPort() {
* Run a command inside a running container, as though using "docker exec", and interpreting
* the output as UTF8.
* <p>
* @see #execInContainer(Charset, String...)
* @see ExecInContainerPattern#execInContainer(com.github.dockerjava.api.command.InspectContainerResponse, String...)
*/
ExecResult execInContainer(String... command)
throws UnsupportedOperationException, IOException, InterruptedException;

/**
* Run a command inside a running container, as though using "docker exec".
* <p>
* This functionality is not available on a docker daemon running the older "lxc" execution driver. At
* the time of writing, CircleCI was using this driver.
* @param outputCharset the character set used to interpret the output.
* @param command the parts of the command to run
* @return the result of execution
* @throws IOException if there's an issue communicating with Docker
* @throws InterruptedException if the thread waiting for the response is interrupted
* @throws UnsupportedOperationException if the docker daemon you're connecting to doesn't support "exec".
* @see ExecInContainerPattern#execInContainer(com.github.dockerjava.api.command.InspectContainerResponse, Charset, String...)
*/
ExecResult execInContainer(Charset outputCharset, String... command)
throws UnsupportedOperationException, IOException, InterruptedException;
Expand All @@ -460,8 +423,6 @@ ExecResult execInContainer(Charset outputCharset, String... command)
*/
void copyFileFromContainer(String containerPath, String destinationPath) throws IOException, InterruptedException;

List<Integer> getExposedPorts();

List<String> getPortBindings();

List<String> getExtraHosts();
Expand Down Expand Up @@ -496,17 +457,6 @@ ExecResult execInContainer(Charset outputCharset, String... command)
@Deprecated
Info getDockerDaemonInfo();

String getContainerId();

String getContainerName();

/**
*
* @deprecated please use {@code org.testcontainers.DockerClientFactory.instance().client().inspectContainerCmd(container.getContainerId()).exec()}
*/
@Deprecated
InspectContainerResponse getContainerInfo();

void setExposedPorts(List<Integer> exposedPorts);

void setPortBindings(List<String> portBindings);
Expand Down
114 changes: 114 additions & 0 deletions core/src/main/java/org/testcontainers/containers/ContainerState.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package org.testcontainers.containers;

import com.github.dockerjava.api.command.InspectContainerResponse;
import com.github.dockerjava.api.exception.DockerException;
import com.github.dockerjava.api.model.ExposedPort;
import com.github.dockerjava.api.model.PortBinding;
import com.github.dockerjava.api.model.Ports;
import com.google.common.base.Preconditions;
import org.testcontainers.DockerClientFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public interface ContainerState {

/**
* Get the IP address that this container may be reached on (may not be the local machine).
*
* @return an IP address
*/
default String getContainerIpAddress() {
return DockerClientFactory.instance().dockerHostIpAddress();
}

/**
* @return is the container currently running?
*/
default Boolean isRunning() {
try {
return getContainerId() != null && DockerClientFactory.instance().client().inspectContainerCmd(getContainerId()).exec().getState().getRunning();
} catch (DockerException e) {
return false;
}
}

/**
* Get the actual mapped port for a first port exposed by the container.
*
* @return the port that the exposed port is mapped to
* @throws IllegalStateException if there are no exposed ports
*/
default Integer getFirstMappedPort() {
return getExposedPorts()
.stream()
.findFirst()
.map(this::getMappedPort)
.orElseThrow(() -> new IllegalStateException("Container doesn't expose any ports"));
}

/**
* Get the actual mapped port for a given port exposed by the container.
*
* @param originalPort the original TCP port that is exposed
* @return the port that the exposed port is mapped to, or null if it is not exposed
*/
default Integer getMappedPort(int originalPort) {
Preconditions.checkState(this.getContainerId() != null, "Mapped port can only be obtained after the container is started");

Ports.Binding[] binding = new Ports.Binding[0];
final InspectContainerResponse containerInfo = this.getContainerInfo();
if (containerInfo != null) {
binding = containerInfo.getNetworkSettings().getPorts().getBindings().get(new ExposedPort(originalPort));
}

if (binding != null && binding.length > 0 && binding[0] != null) {
return Integer.valueOf(binding[0].getHostPortSpec());
} else {
throw new IllegalArgumentException("Requested port (" + originalPort + ") is not mapped");
}
}

/**
* @return the exposed ports
*/
List<Integer> getExposedPorts();

/**
* @return the port bindings
*/
default List<String> getPortBindings() {
List<String> portBindings = new ArrayList<>();
final Ports hostPortBindings = this.getContainerInfo().getHostConfig().getPortBindings();
for (Map.Entry<ExposedPort, Ports.Binding[]> binding : hostPortBindings.getBindings().entrySet()) {
for (Ports.Binding portBinding : binding.getValue()) {
portBindings.add(String.format("%s:%s", portBinding.toString(), binding.getKey()));
}
}
return portBindings;
}

/**
* @return the bound port numbers
*/
default List<Integer> getBoundPortNumbers() {
return getPortBindings().stream()
.map(PortBinding::parse)
.map(PortBinding::getBinding)
.map(Ports.Binding::getHostPortSpec)
.map(Integer::valueOf)
.collect(Collectors.toList());
}

/**
* @return the id of the container
*/
String getContainerId();

/**
* @return the container info
*/
InspectContainerResponse getContainerInfo();
}
Loading

0 comments on commit 8558203

Please sign in to comment.